Code:
/ Net / Net / 3.5.50727.3053 / DEVDIV / depot / DevDiv / releases / Orcas / SP / wpf / src / Framework / System / Windows / Controls / VirtualizingStackPanel.cs / 3 / VirtualizingStackPanel.cs
//----------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation. All rights reserved.
//
//---------------------------------------------------------------------------
//#define Profiling
using MS.Internal;
using MS.Internal.Controls;
using MS.Utility;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using System.Windows.Input;
namespace System.Windows.Controls
{
///
/// VirtualizingStackPanel is used to arrange children into single line.
///
public class VirtualizingStackPanel : VirtualizingPanel, IScrollInfo
{
//-------------------------------------------------------------------
//
// Constructors
//
//-------------------------------------------------------------------
#region Constructors
///
/// Default constructor.
///
public VirtualizingStackPanel()
{
}
static VirtualizingStackPanel()
{
lock (DependencyProperty.Synchronized)
{
_desiredSizeStorageIndex = DependencyProperty.GetUniqueGlobalIndex(null, null);
DependencyProperty.RegisteredPropertyList.Add();
}
}
#endregion Constructors
//--------------------------------------------------------------------
//
// Public Methods
//
//-------------------------------------------------------------------
#region Public Methods
//------------------------------------------------------------
// IScrollInfo Methods
//------------------------------------------------------------
#region IScrollInfo Methods
///
/// Scroll content by one line to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineUp()
{
SetVerticalOffset(VerticalOffset - ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineDown()
{
SetVerticalOffset(VerticalOffset + ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineRight()
{
SetHorizontalOffset(HorizontalOffset + ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageUp()
{
SetVerticalOffset(VerticalOffset - ViewportHeight);
}
///
/// Scroll content by one page to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageDown()
{
SetVerticalOffset(VerticalOffset + ViewportHeight);
}
///
/// Scroll content by one page to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageLeft()
{
SetHorizontalOffset(HorizontalOffset - ViewportWidth);
}
///
/// Scroll content by one page to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageRight()
{
SetHorizontalOffset(HorizontalOffset + ViewportWidth);
}
///
/// Scroll content by one page to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - SystemParameters.WheelScrollLines * ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + SystemParameters.WheelScrollLines * ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelLeft()
{
SetHorizontalOffset(HorizontalOffset - 3.0 * ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelRight()
{
SetHorizontalOffset(HorizontalOffset + 3.0 * ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Set the HorizontalOffset to the passed value.
///
public void SetHorizontalOffset(double offset)
{
EnsureScrollData();
double scrollX = ScrollContentPresenter.ValidateInputOffset(offset, "HorizontalOffset");
if (!DoubleUtil.AreClose(scrollX, _scrollData._offset.X))
{
Vector oldViewportOffset = _scrollData._offset;
// Store the new offset
_scrollData._offset.X = scrollX;
// Report the change in offset
OnViewportOffsetChanged(oldViewportOffset, _scrollData._offset);
InvalidateMeasure();
}
}
///
/// Set the VerticalOffset to the passed value.
///
public void SetVerticalOffset(double offset)
{
EnsureScrollData();
double scrollY = ScrollContentPresenter.ValidateInputOffset(offset, "VerticalOffset");
if (!DoubleUtil.AreClose(scrollY, _scrollData._offset.Y))
{
Vector oldViewportOffset = _scrollData._offset;
// Store the new offset
_scrollData._offset.Y = scrollY;
// Report the change in offset
OnViewportOffsetChanged(oldViewportOffset, _scrollData._offset);
InvalidateMeasure();
}
}
///
/// VirtualizingStackPanel implementation of .
///
// The goal is to change offsets to bring the child into view, and return a rectangle in our space to make visible.
// The rectangle we return is in the physical dimension the input target rect transformed into our pace.
// In the logical dimension, it is our immediate child's rect.
// Note: This code presently assumes we/children are layout clean. See work item 22269 for more detail.
public Rect MakeVisible(Visual visual, Rect rectangle)
{
Vector newOffset = new Vector();
Rect newRect = new Rect();
Rect originalRect = rectangle;
bool isHorizontal = (Orientation == Orientation.Horizontal);
// We can only work on visuals that are us or children.
// An empty rect has no size or position. We can't meaningfully use it.
if ( rectangle.IsEmpty
|| visual == null
|| visual == (Visual)this
|| !this.IsAncestorOf(visual))
{
return Rect.Empty;
}
#pragma warning disable 1634, 1691
#pragma warning disable 56506
// Compute the child's rect relative to (0,0) in our coordinate space.
// This is a false positive by PreSharp. visual cannot be null because of the 'if' check above
GeneralTransform childTransform = visual.TransformToAncestor(this);
#pragma warning restore 56506
#pragma warning restore 1634, 1691
rectangle = childTransform.TransformBounds(rectangle);
// We can't do any work unless we're scrolling.
if (!IsScrolling)
{
return rectangle;
}
// Make ourselves visible in the non-stacking direction
MakeVisiblePhysicalHelper(rectangle, ref newOffset, ref newRect, !isHorizontal);
if (IsPixelBased)
{
MakeVisiblePhysicalHelper(rectangle, ref newOffset, ref newRect, isHorizontal);
}
else
{
// Bring our child containing the visual into view.
// For non-pixel based scrolling the offset is in logical units in the stacking direction
// and physical units in the other. Hence the logical helper call here.
int childIndex = FindChildIndexThatParentsVisual(visual);
MakeVisibleLogicalHelper(childIndex, rectangle, ref newOffset, ref newRect);
}
// We have computed the scrolling offsets; validate and scroll to them.
newOffset.X = ScrollContentPresenter.CoerceOffset(newOffset.X, _scrollData._extent.Width, _scrollData._viewport.Width);
newOffset.Y = ScrollContentPresenter.CoerceOffset(newOffset.Y, _scrollData._extent.Height, _scrollData._viewport.Height);
if (!DoubleUtil.AreClose(newOffset, _scrollData._offset))
{
Vector oldOffset = _scrollData._offset;
_scrollData._offset = newOffset;
OnViewportOffsetChanged(oldOffset, newOffset);
InvalidateMeasure();
OnScrollChange();
if (ScrollOwner != null)
{
// When layout gets updated it may happen that visual is obscured by a ScrollBar
// We call MakeVisible again to make sure element is visible in this case
ScrollOwner.MakeVisible(visual, originalRect);
}
_bringIntoViewContainer = null;
}
// Return the rectangle
return newRect;
}
///
/// Generates the item at the specified index and calls BringIntoView on it.
///
/// Specify the item index that should become visible
///
/// Thrown if index is out of range
///
protected internal override void BringIndexIntoView(int index)
{
if (index < 0 || index >= ItemCount)
throw new ArgumentOutOfRangeException("index");
IItemContainerGenerator generator = Generator;
int childIndex;
bool visualOrderChanged = false;
GeneratorPosition position = IndexToGeneratorPositionForStart(index, out childIndex);
using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
bool newlyRealized;
UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
if (child != null)
{
visualOrderChanged = AddContainerFromGenerator(childIndex, child, newlyRealized);
if (visualOrderChanged)
{
Debug.Assert(IsVirtualizing && InRecyclingMode, "We should only modify the visual order when in recycling mode");
InvalidateZState();
}
FrameworkElement element = child as FrameworkElement;
if (element != null)
{
element.BringIntoView();
_bringIntoViewContainer = element;
}
}
}
}
#endregion
#endregion
//-------------------------------------------------------------------
//
// Public Properties
//
//--------------------------------------------------------------------
#region Public Properties
///
/// Specifies dimension of children stacking.
///
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
///
/// This property is always true because this panel has vertical or horizontal orientation
///
protected internal override bool HasLogicalOrientation
{
get { return true; }
}
///
/// Orientation of the panel if its layout is in one dimension.
/// Otherwise HasLogicalOrientation is false and LogicalOrientation should be ignored
///
protected internal override Orientation LogicalOrientation
{
get { return this.Orientation; }
}
///
/// DependencyProperty for property.
///
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(Orientation.Vertical,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnOrientationChanged)),
new ValidateValueCallback(ScrollBar.IsValidOrientation));
///
/// Attached property for use on the ItemsControl that is the host for the items being
/// presented by this panel. Use this property to turn virtualization on/off.
///
public static readonly DependencyProperty IsVirtualizingProperty =
DependencyProperty.RegisterAttached("IsVirtualizing", typeof(bool), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(true));
///
/// Retrieves the value for .
///
/// The object on which to query the value.
/// True if virtualizing, false otherwise.
public static bool GetIsVirtualizing(DependencyObject o)
{
if (o == null)
{
throw new ArgumentNullException("o");
}
return (bool)o.GetValue(IsVirtualizingProperty);
}
///
/// Sets the value for .
///
/// The element on which to set the value.
/// True if virtualizing, false otherwise.
public static void SetIsVirtualizing(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(IsVirtualizingProperty, value);
}
///
/// Attached property for use on the ItemsControl that is the host for the items being
/// presented by this panel. Use this property to modify the virtualization mode.
///
/// Note that this property can only be set before the panel has been initialized
///
public static readonly DependencyProperty VirtualizationModeProperty =
DependencyProperty.RegisterAttached("VirtualizationMode", typeof(VirtualizationMode), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(VirtualizationMode.Standard));
///
/// Retrieves the value for .
///
/// The object on which to query the value.
/// The current virtualization mode.
public static VirtualizationMode GetVirtualizationMode(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (VirtualizationMode)element.GetValue(VirtualizationModeProperty);
}
///
/// Sets the value for .
///
/// The element on which to set the value.
/// The desired virtualization mode.
public static void SetVirtualizationMode(DependencyObject element, VirtualizationMode value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(VirtualizationModeProperty, value);
}
//-----------------------------------------------------------
// IScrollInfo Properties
//-----------------------------------------------------------
#region IScrollInfo Properties
///
/// VirtualizingStackPanel reacts to this property by changing its child measurement algorithm.
/// If scrolling in a dimension, infinite space is allowed the child; otherwise, available size is preserved.
///
[DefaultValue(false)]
public bool CanHorizontallyScroll
{
get
{
if (_scrollData == null) { return false; }
return _scrollData._allowHorizontal;
}
set
{
EnsureScrollData();
if (_scrollData._allowHorizontal != value)
{
_scrollData._allowHorizontal = value;
InvalidateMeasure();
}
}
}
///
/// VirtualizingStackPanel reacts to this property by changing its child measurement algorithm.
/// If scrolling in a dimension, infinite space is allowed the child; otherwise, available size is preserved.
///
[DefaultValue(false)]
public bool CanVerticallyScroll
{
get
{
if (_scrollData == null) { return false; }
return _scrollData._allowVertical;
}
set
{
EnsureScrollData();
if (_scrollData._allowVertical != value)
{
_scrollData._allowVertical = value;
InvalidateMeasure();
}
}
}
///
/// ExtentWidth contains the horizontal size of the scrolled content element in 1/96"
///
public double ExtentWidth
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._extent.Width;
}
}
///
/// ExtentHeight contains the vertical size of the scrolled content element in 1/96"
///
public double ExtentHeight
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._extent.Height;
}
}
///
/// ViewportWidth contains the horizontal size of content's visible range in 1/96"
///
public double ViewportWidth
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._viewport.Width;
}
}
///
/// ViewportHeight contains the vertical size of content's visible range in 1/96"
///
public double ViewportHeight
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._viewport.Height;
}
}
///
/// HorizontalOffset is the horizontal offset of the scrolled content in 1/96".
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public double HorizontalOffset
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._computedOffset.X;
}
}
///
/// VerticalOffset is the vertical offset of the scrolled content in 1/96".
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public double VerticalOffset
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._computedOffset.Y;
}
}
///
/// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant
/// on this IScrollInfo's properties.
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ScrollViewer ScrollOwner
{
get
{
EnsureScrollData();
return _scrollData._scrollOwner;
}
set
{
EnsureScrollData();
if (value != _scrollData._scrollOwner)
{
ResetScrolling(this);
_scrollData._scrollOwner = value;
}
}
}
#endregion IScrollInfo Properties
#endregion Public Properties
//-------------------------------------------------------------------
//
// Public Events
//
//--------------------------------------------------------------------
#region Public Events
///
/// Called on the ItemsControl that owns this panel when an item is being re-virtualized.
///
public static readonly RoutedEvent CleanUpVirtualizedItemEvent = EventManager.RegisterRoutedEvent("CleanUpVirtualizedItemEvent", RoutingStrategy.Direct, typeof(CleanUpVirtualizedItemEventHandler), typeof(VirtualizingStackPanel));
///
/// Adds a handler for the CleanUpVirtualizedItem attached event
///
/// DependencyObject that listens to this event
/// Event Handler to be added
public static void AddCleanUpVirtualizedItemHandler(DependencyObject element, CleanUpVirtualizedItemEventHandler handler)
{
FrameworkElement.AddHandler(element, CleanUpVirtualizedItemEvent, handler);
}
///
/// Removes a handler for the CleanUpVirtualizedItem attached event
///
/// DependencyObject that listens to this event
/// Event Handler to be removed
public static void RemoveCleanUpVirtualizedItemHandler(DependencyObject element, CleanUpVirtualizedItemEventHandler handler)
{
FrameworkElement.RemoveHandler(element, CleanUpVirtualizedItemEvent, handler);
}
///
/// Called when an item is being re-virtualized.
///
protected virtual void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs e)
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (itemsControl != null)
{
itemsControl.RaiseEvent(e);
}
}
#endregion
//-------------------------------------------------------------------
//
// Protected Methods
//
//--------------------------------------------------------------------
#region Protected Methods
///
/// General VirtualizingStackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
/// Children in this dimension are encouraged to be as large as they like. In the other dimension,
/// VirtualizingStackPanel will assume the maximum size of its children.
///
///
/// When scrolling, VirtualizingStackPanel will not grow in layout size but effectively add the children on a z-plane which
/// will probably be clipped by some parent (typically a ScrollContentPresenter) to Stack's size.
///
/// Constraint
/// Desired size
protected override Size MeasureOverride(Size constraint)
{
#if Profiling
if (Panel.IsAboutToGenerateContent(this))
return MeasureOverrideProfileStub(constraint);
else
return RealMeasureOverride(constraint);
}
// this is a handy place to start/stop profiling
private Size MeasureOverrideProfileStub(Size constraint)
{
return RealMeasureOverride(constraint);
}
private Size RealMeasureOverride(Size constraint)
{
#endif
bool etwTracingEnabled = IsScrolling && EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal);
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.StartEvent, "VirtualizingStackPanel :MeasureOverride");
}
Debug.Assert(MeasureData == null || constraint == MeasureData.AvailableSize, "MeasureData needs to be passed down in [....] with size");
MeasureData measureData = MeasureData;
Size stackDesiredSize = new Size();
Size layoutSlotSize = constraint;
bool fHorizontal = (Orientation == Orientation.Horizontal);
int firstViewport; // First child index in the viewport.
double firstItemOffset; // Offset of the top of the first child relative to the top of the viewport.
double virtualizedItemsSize = 0d; // Amount that virtualized children contribute to the desired size in the stacking direction
int lastViewport = -1; // Last child index in the viewport. -1 indicates we have not yet iterated through the last child.
double logicalVisibleSpace, childLogicalSize;
Rect originalViewport = Rect.Empty; // Only used if this is the scrolling panel. Saves off the given viewport for scroll computations.
// Collect information from the ItemsControl, if there is one.
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = (itemsControl != null) ? itemsControl.Items.Count : 0;
SetVirtualizationState(itemsControl, /* hasMeasureData = */ measureData != null && measureData.HasViewport);
IList children = RealizedChildren; // yes, this is weird, but this property ensures the Generator is properly initialized.
IItemContainerGenerator generator = Generator;
// Adjust the viewport
if (IsPixelBased)
{
if (IsScrolling)
{
// We're the top level scrolling panel. Set the viewport and extend it to add a focus trail
originalViewport = new Rect(_scrollData._offset.X, _scrollData._offset.Y, constraint.Width, constraint.Height);
measureData = new MeasureData(constraint, originalViewport);
// The way we have a focus trail when pixel-based is to artificially extend the viewport. All calculations are done
// with this 'artificial' viewport with the exception of the scroll offset, extent, etc.
measureData = AddFocusTrail(measureData, fHorizontal);
Debug.Assert(!object.ReferenceEquals(originalViewport, measureData.Viewport), "original viewport should not have a focus trail");
}
else
{
measureData = AdjustViewportOffset(measureData, itemsControl, fHorizontal);
Debug.Assert(!object.ReferenceEquals(MeasureData, measureData), "The value set in the MeasureData property should not be modified");
}
}
//
// Initialize child sizing and iterator data
// Allow children as much size as they want along the stack.
//
if (fHorizontal)
{
layoutSlotSize.Width = Double.PositiveInfinity;
if (IsScrolling && CanVerticallyScroll) { layoutSlotSize.Height = Double.PositiveInfinity; }
logicalVisibleSpace = constraint.Width;
}
else
{
layoutSlotSize.Height = Double.PositiveInfinity;
if (IsScrolling && CanHorizontallyScroll) { layoutSlotSize.Width = Double.PositiveInfinity; }
logicalVisibleSpace = constraint.Height;
}
// Compute index of first item in the viewport
firstViewport = ComputeIndexOfFirstVisibleItem(measureData, itemsControl, fHorizontal, out firstItemOffset);
if (IsPixelBased)
{
// Acount for the size of items that won't be generated
Debug.Assert(stackDesiredSize.Width == 0 && stackDesiredSize.Height == 0, "stack desired size must be 0 for virtualizedItemsSize to work");
stackDesiredSize = ExtendDesiredSize(itemsControl, stackDesiredSize, firstViewport, /*before = */ true, fHorizontal);
virtualizedItemsSize = fHorizontal ? stackDesiredSize.Width : stackDesiredSize.Height;
}
debug_AssertRealizedChildrenEqualVisualChildren();
// If recycling clean up before generating children.
if (IsVirtualizing && InRecyclingMode)
{
CleanupContainers(firstViewport, itemsControl);
debug_VerifyRealizedChildren();
}
//
// Figure out the position of the first visible item
//
GeneratorPosition startPos = IndexToGeneratorPositionForStart(IsVirtualizing ? firstViewport : 0, out _firstVisibleChildIndex);
int childIndex = _firstVisibleChildIndex;
//
// Main loop: generate and measure all children (or all visible children if virtualizing).
//
bool ranOutOfItems = true;
bool visualOrderChanged = false;
_visibleCount = 0;
if (itemCount > 0)
{
_afterTrail = 0;
using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
for (int i = IsVirtualizing ? firstViewport : 0, count = itemCount; i < count; ++i)
{
// Get next child.
bool newlyRealized;
UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
if (child == null)
{
Debug.Assert(!newlyRealized, "The generator realized a null value.");
// We reached the end of the items (because of a group)
break;
}
visualOrderChanged |= AddContainerFromGenerator(childIndex, child, newlyRealized);
childIndex++;
_visibleCount++;
if (IsPixelBased)
{
// Pass along MeasureData so it continues down the tree.
child.MeasureData = CreateChildMeasureData(measureData, layoutSlotSize, stackDesiredSize, fHorizontal);
}
Size childDesiredSize = child.DesiredSize;
child.Measure(layoutSlotSize);
if (childDesiredSize != child.DesiredSize)
{
childDesiredSize = child.DesiredSize;
// Reset the _maxDesiredSize cache if child DesiredSize changes
if (_scrollData != null)
_scrollData._maxDesiredSize = new Size();
}
// Accumulate child size.
if (fHorizontal)
{
stackDesiredSize.Width += childDesiredSize.Width;
stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height);
childLogicalSize = childDesiredSize.Width;
}
else
{
stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
stackDesiredSize.Height += childDesiredSize.Height;
childLogicalSize = childDesiredSize.Height;
}
// Adjust remaining viewport space if we are scrolling and within the viewport region.
// While scrolling (not virtualizing), we always measure children before and after the viewport.
if (IsScrolling && lastViewport == -1 && i >= firstViewport)
{
logicalVisibleSpace -= childLogicalSize;
if (DoubleUtil.LessThanOrClose(logicalVisibleSpace, 0.0))
{
lastViewport = i;
}
}
// When under a viewport, virtualizing and at or beyond the first element, stop creating elements when out of space.
if (IsVirtualizing && (i >= firstViewport))
{
double viewportSize;
double totalGenerated;
//
// Decide if the end of the item is outside the viewport.
//
// StackDesiredSize, with some adjustment, is a measure of exactly how much viewport space we have used.
//
// StackDesiredSize is the sum of all generated children (starting with the first visible item). The first
// visible item doesn't always start at the top of the viewport, so we have to adjust by the firstItemoffset.
//
// When pixel-based we add the sum of all virtualized children to the stackDesiredSize; this has to be removed as well.
//
Debug.Assert(IsPixelBased || virtualizedItemsSize == 0d);
if (fHorizontal)
{
viewportSize = IsPixelBased ? measureData.Viewport.Width : constraint.Width;
totalGenerated = stackDesiredSize.Width - virtualizedItemsSize + firstItemOffset;
}
else
{
viewportSize = IsPixelBased ? measureData.Viewport.Height : constraint.Height;
totalGenerated = stackDesiredSize.Height - virtualizedItemsSize + firstItemOffset;
}
if (totalGenerated > viewportSize)
{
// The end of this child is outside the viewport. Check if we want to generate some more.
if (IsPixelBased)
{
// For pixel-based virtualization (specifically TreeView virtualization) we deal with
// the after trail later, since it has to function hierarchically.
break;
}
else
{
// We want to keep a focusable item after the end so that keyboard navigation
// can work, but we want to limit that to FocusTrail number of items
// in case all the items are not focusable.
if (_afterTrail > 0 && ( _afterTrail >= FocusTrail || Keyboard.IsFocusable(child)))
{
// Either we passed the limit or the child was focusable
ranOutOfItems = false;
break;
}
_afterTrail++;
// Loop around and generate another item
}
}
}
}
}
}
#if DEBUG
if (IsVirtualizing && InRecyclingMode)
{
debug_VerifyRealizedChildren();
}
#endif
_visibleStart = firstViewport;
if (IsPixelBased)
{
// Acount for the size of items that won't be generated
stackDesiredSize = ExtendDesiredSize(itemsControl, stackDesiredSize, firstViewport + _visibleCount, /*before = */ false, fHorizontal);
}
//
// Adjust the scroll offset, extent, etc.
//
if (IsScrolling)
{
if (IsPixelBased)
{
Vector offset = new Vector(originalViewport.Location.X, originalViewport.Location.Y);
SetAndVerifyScrollingData(originalViewport.Size, stackDesiredSize, offset);
}
else
{
// Compute the extent before we fill remaining space and modify the stack desired size
Size extent = ComputeLogicalExtent(stackDesiredSize, itemCount, fHorizontal);
if (ranOutOfItems)
{
// If we or children have resized, it's possible that we can now display more content.
// This is true if we started at a nonzero offeset and still have space remaining.
// In this case, we loop back through previous children until we run out of space.
FillRemainingSpace(ref firstViewport, ref logicalVisibleSpace, ref stackDesiredSize, layoutSlotSize, fHorizontal);
}
// Create the Before focus trail
// NOTE: the call here (under IsScrolling) implicitly assumes that only a scrolling panel can virtualize and thus requires
// a focus trail. That's not true for hierarchical (pixel-based) virtualization, but it handles the focus trail differently anyway.
EnsureTopCapGenerated(layoutSlotSize);
// Compute Scrolling data such as extent, viewport, and offset.
stackDesiredSize = UpdateLogicalScrollData(stackDesiredSize, constraint, logicalVisibleSpace,
extent, firstViewport, lastViewport, itemCount, fHorizontal);
}
}
//
// Cleanup items no longer in the viewport
//
if (IsVirtualizing && !InRecyclingMode)
{
if (IsPixelBased)
{
// Immediate cleanup
CleanupContainers(firstViewport, itemsControl);
}
else
{
// Less aggressive backwards-compat background cleanup operation
EnsureCleanupOperation(false /* delay */);
}
}
if (IsVirtualizing && InRecyclingMode)
{
DisconnectRecycledContainers();
if (visualOrderChanged)
{
// We moved some containers in the visual tree without firing changed events. ZOrder is now invalid.
InvalidateZState();
}
}
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.EndEvent, "VirtualizingStackPanel :MeasureOverride");
}
debug_AssertRealizedChildrenEqualVisualChildren();
return stackDesiredSize;
}
///
/// Content arrangement.
///
/// Arrange size
protected override Size ArrangeOverride(Size arrangeSize)
{
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(arrangeSize);
IList children;
double previousChildSize = 0.0;
ItemsControl itemsControl = null;
bool childrenAreContainers = true;
bool etwTracingEnabled = IsScrolling && EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal);
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.StartEvent, "VirtualizingStackPanel :ArrangeOverride");
}
//
// Compute scroll offset and seed it into rcChild.
//
if (IsScrolling)
{
if (fHorizontal)
{
double offsetX = _scrollData._computedOffset.X;
rcChild.X = IsPixelBased ? -offsetX : ComputePhysicalFromLogicalOffset(IsVirtualizing ? _firstVisibleChildIndex : offsetX, true);
rcChild.Y = -1.0 * _scrollData._computedOffset.Y;
}
else
{
double offsetY = _scrollData._computedOffset.Y;
rcChild.X = -1.0 * _scrollData._computedOffset.X;
rcChild.Y = IsPixelBased ? -offsetY : ComputePhysicalFromLogicalOffset(IsVirtualizing ? _firstVisibleChildIndex : offsetY, false);
}
}
//
// Arrange and Position Children.
//
// If we're virtualizing and pixel-based we loop through the entire items collection (the policy is to arrange items exactly where they
// should appear regardless of the virtualization state of siblings). This is required to properly virtualize hiearchically.
// Otherwise we loop through the children collection (when virtualizing in items mode VSP arranges children in a simple stack order).
//
if (IsPixelBased && IsVirtualizing)
{
// This is a pixel-based internal panel. It must behave externally exactly the way a non-virtualizing panel does in Arrange.
// Specifically, it arranges its children in the 'proper' place, regardless of whether or not their siblings are virtualized.
itemsControl = ItemsControl.GetItemsOwner(this);
children = itemsControl.Items;
childrenAreContainers = false;
}
else
{
debug_AssertRealizedChildrenEqualVisualChildren(); // RealizedChildren only differs from InternalChildren inside of Measure when container recycling is on.
children = RealizedChildren;
}
for (int i = 0; i < children.Count; ++i)
{
UIElement container = null;
Size childSize;
if (childrenAreContainers)
{
// we are looping through the actual containers; the visual children of this panel.
container = (UIElement)children[i];
childSize = container.DesiredSize;
}
else
{
// We are looping through items and may or may not have a container for each given item.
childSize = ContainerSizeForItem(itemsControl, children[i], i, out container);
}
if (fHorizontal)
{
rcChild.X += previousChildSize;
previousChildSize = childSize.Width;
rcChild.Width = previousChildSize;
rcChild.Height = Math.Max(arrangeSize.Height, childSize.Height);
}
else
{
rcChild.Y += previousChildSize;
previousChildSize = childSize.Height;
rcChild.Height = previousChildSize;
rcChild.Width = Math.Max(arrangeSize.Width, childSize.Width);
}
if (container != null)
{
container.Arrange(rcChild);
}
}
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.EndEvent, "VirtualizingStackPanel :ArrangeOverride");
}
return arrangeSize;
}
///
/// Called when the Items collection associated with the containing ItemsControl changes.
///
/// sender
/// Event arguments
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
base.OnItemsChanged(sender, args);
bool resetMaximumDesiredSize = false;
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
OnItemsRemove(args);
resetMaximumDesiredSize = true;
break;
case NotifyCollectionChangedAction.Replace:
OnItemsReplace(args);
resetMaximumDesiredSize = true;
break;
case NotifyCollectionChangedAction.Move:
OnItemsMove(args);
break;
case NotifyCollectionChangedAction.Reset:
resetMaximumDesiredSize = true;
break;
}
if (resetMaximumDesiredSize && IsScrolling)
{
// The items changed such that the maximum size may no longer be valid.
// The next layout pass will update this value.
_scrollData._maxDesiredSize = new Size();
}
}
///
/// Called when the UI collection of children is cleared by the base Panel class.
///
protected override void OnClearChildren()
{
base.OnClearChildren();
_realizedChildren = null;
_visibleStart = _firstVisibleChildIndex = _visibleCount = 0;
}
// Override of OnGotKeyboardFocus. Called when focus moves to any child or subchild of this VSP
// Used by TreeView virtualization to keep track of the focused item.
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
FocusChanged(e);
}
// Override of OnLostKeyboardFocus. Called when focus moves away from this VSP.
// Used by TreeView virtualization to keep track of the focused item.
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
FocusChanged(e);
}
#endregion Protected Methods
#region Internal Methods
// Tells the Generator to clear out all containers for this ItemsControl. This is called by the ItemValueStorage
// service when the ItemsControl this panel is a host for is about to be thrown away. This allows the VSP to save
// off any properties it is interested in and results in a call to ClearContainerForItem on the ItemsControl, allowing
// the Item Container Storage to do so as well.
// Note: A possible perf improvement may be to make 'fast' RemoveAll on the Generator that simply calls ClearContainerForItem
// for us without walking through its data structures to actually clean out items.
internal void ClearAllContainers(ItemsControl itemsControl)
{
Debug.Assert(IsVirtualizing,
"We should only clear containers for ItemsControls that are virtualizing");
Debug.Assert(itemsControl == ItemsControl.GetItemsOwner(this),
"We can only clear containers that this panel is a host for");
IItemContainerGenerator generator = Generator;
if (IsPixelBased)
{
IList children = RealizedChildren;
UIElement child;
for (int i = 0; i < children.Count; i++)
{
child = (UIElement)children[i];
itemsControl.StoreItemValue(((ItemContainerGenerator)generator).ItemFromContainer(child), child.DesiredSize, _desiredSizeStorageIndex);
}
}
if (generator != null)
{
generator.RemoveAll();
}
}
#endregion
//------------------------------------------------------
//
// Private Methods
//
//-----------------------------------------------------
#region Private Methods
//
// MeasureOverride Helpers
//
#region MeasureOverride Helpers
///
/// Extends the viewport of the given MeasureData to give a focus trail. Returns by how much it extended the viewport.
///
///
///
private MeasureData AddFocusTrail(MeasureData measureData, bool isHorizontal)
{
//
// Create the before / after focus trail for interior panels that use MeasureData's viewport to virtualize.
// We expand the viewport so that roughly two extra items are generated at the top and the bottom.
//
// For the before / after focus trail good values are
// padding = header height * 4;
//
// To make page up / down work without rewriting TreeView's algorithm we actually extend the viewport one extra page
// top and bottm.
//
Debug.Assert(IsScrolling, "The scrolling panel is the only one that should extend the viewport");
Invariant.Assert(IsPixelBased, "If we're sending down a viewport to the children we should be doing pixel-based computations");
double page = isHorizontal ? ViewportWidth : ViewportHeight;
Rect viewport = measureData.Viewport;
if (isHorizontal)
{
viewport.Width += page * 2;
viewport.X -= page;
}
else
{
viewport.Height += page * 2;
viewport.Y -= page;
}
measureData.Viewport = viewport;
return measureData;
}
#region Scroll Computation Helpers
///
/// Returns the extent in logical units in the stacking direction.
///
///
///
///
///
private Size ComputeLogicalExtent(Size stackDesiredSize, int itemCount, bool isHorizontal)
{
bool accumulateExtent = false;
Size extent = new Size();
if (ScrollOwner != null)
{
accumulateExtent = ScrollOwner.InChildInvalidateMeasure;
ScrollOwner.InChildInvalidateMeasure = false;
}
if (isHorizontal)
{
extent.Width = itemCount;
extent.Height = accumulateExtent ? Math.Max(stackDesiredSize.Height, _scrollData._extent.Height) : stackDesiredSize.Height;
}
else
{
extent.Width = accumulateExtent ? Math.Max(stackDesiredSize.Width, _scrollData._extent.Width) : stackDesiredSize.Width;
extent.Height = itemCount;
}
return extent;
}
///
/// Called when we ran out of children before filling up the viewport.
///
private void FillRemainingSpace(ref int firstViewport, ref double logicalVisibleSpace, ref Size stackDesiredSize, Size layoutSlotSize, bool isHorizontal)
{
Debug.Assert(IsScrolling, "Only the scrolling panel can fill remaining space");
Debug.Assert(!IsPixelBased, "This is a logical operation");
double projectedLogicalVisibleSpace;
Size childDesiredSize;
IList children = RealizedChildren;
int childIndex = IsVirtualizing ? _firstVisibleChildIndex : firstViewport;
while (childIndex > 0)
{
if (!PreviousChildIsGenerated(childIndex))
{
GeneratePreviousChild(childIndex, layoutSlotSize);
childIndex++; // We just inserted a child, so increment the index
}
else if (childIndex <= _firstVisibleChildIndex)
{
((UIElement)children[childIndex - 1]).Measure(layoutSlotSize);
}
projectedLogicalVisibleSpace = logicalVisibleSpace;
childDesiredSize = ((UIElement)children[childIndex - 1]).DesiredSize;
if (isHorizontal)
{
projectedLogicalVisibleSpace -= childDesiredSize.Width;
}
else
{
projectedLogicalVisibleSpace -= childDesiredSize.Height;
}
// If we have run out of room, break.
if (DoubleUtil.LessThan(projectedLogicalVisibleSpace, 0.0)) { break; }
// Account for the child in the panel's desired size
if (isHorizontal)
{
stackDesiredSize.Width += childDesiredSize.Width;
stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height);
}
else
{
stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
stackDesiredSize.Height += childDesiredSize.Height;
}
// Adjust viewport
childIndex--;
logicalVisibleSpace = projectedLogicalVisibleSpace;
_visibleCount++;
}
if ((childIndex < _firstVisibleChildIndex) || !IsVirtualizing)
{
_firstVisibleChildIndex = childIndex;
}
_visibleStart = firstViewport = (IsItemsHost && children.Count != 0) ? GetGeneratedIndex(_firstVisibleChildIndex) : 0;
}
///
/// Updates ScrollData's offset, extent, and viewport in logical units.
///
///
///
///
///
///
///
///
///
///
private Size UpdateLogicalScrollData(Size stackDesiredSize, Size constraint, double logicalVisibleSpace, Size extent,
int firstViewport, int lastViewport, int itemCount, bool fHorizontal)
{
Debug.Assert(IsScrolling && !IsPixelBased, "this computes logical scroll data");
Size viewport = constraint;
Vector offset = _scrollData._offset;
// If we have not yet set the last child in the viewport, set it to the last child.
if (lastViewport == -1) { lastViewport = itemCount - 1; }
int logicalExtent = itemCount;
int logicalViewport = lastViewport - firstViewport;
//
// Compute the logical viewport size.
//
// We are conservative when estimating a viewport, not including the last element in case it is only partially visible.
// We want to count it if it is fully visible (>= 0 space remaining) or the only element in the viewport.
if (logicalViewport == 0 || DoubleUtil.GreaterThanOrClose(logicalVisibleSpace, 0.0)) { logicalViewport++; }
if (fHorizontal)
{
viewport.Width = logicalViewport;
offset.X = firstViewport;
offset.Y = Math.Max(0, Math.Min(offset.Y, extent.Height - viewport.Height));
// In case last item is visible because we scroll all the way to the right and scrolling is on
// we want desired size not to be smaller than constraint to avoid another relayout
if (logicalExtent > logicalViewport && !Double.IsPositiveInfinity(constraint.Width))
{
stackDesiredSize.Width = constraint.Width;
}
}
else
{
viewport.Height = logicalViewport;
offset.Y = firstViewport;
offset.X = Math.Max(0, Math.Min(offset.X, extent.Width - viewport.Width));
// In case last item is visible because we scroll all the way to the bottom and scrolling is on
// we want desired size not to be smaller than constraint to avoid another relayout
if (logicalExtent > logicalViewport && !Double.IsPositiveInfinity(constraint.Height))
{
stackDesiredSize.Height = constraint.Height;
}
}
// Since we can offset and clip our content, we never need to be larger than the parent suggestion.
// If we returned the full size of the content, we would always be so big we didn't need to scroll. :)
stackDesiredSize.Width = Math.Min(stackDesiredSize.Width, constraint.Width);
stackDesiredSize.Height = Math.Min(stackDesiredSize.Height, constraint.Height);
// When scrolling, the maximum horizontal or vertical size of items can cause the desired size of the
// panel to change, which can cause the owning ScrollViewer re-layout as well when it is not necessary.
// We will thus remember the maximum desired size and always return that. The actual arrangement and
// clipping still be calculated from actual scroll data values.
// The maximum desired size is reset when the items change.
_scrollData._maxDesiredSize.Width = Math.Max(stackDesiredSize.Width, _scrollData._maxDesiredSize.Width);
_scrollData._maxDesiredSize.Height = Math.Max(stackDesiredSize.Height, _scrollData._maxDesiredSize.Height);
stackDesiredSize = _scrollData._maxDesiredSize;
// Verify Scroll Info, invalidate ScrollOwner if necessary.
SetAndVerifyScrollingData(viewport, extent, offset);
return stackDesiredSize;
}
#endregion
///
/// DesiredSize is normally computed by summing up the size of all items we've generated. Pixel-based virtualization uses a 'full' desired size.
/// This extends the given desired size beyond the visible items. It will extend it by the items before or after the set of generated items.
/// The given pivotIndex is the index of either the first or last item generated.
///
///
///
///
///
///
///
private Size ExtendDesiredSize(ItemsControl itemsControl, Size stackDesiredSize, int pivotIndex, bool before, bool isHorizontal)
{
Debug.Assert(IsPixelBased, "MeasureOverride should have already computed desiredSize if non-virtualizing or items-based");
//
// If we're virtualizing the sum of all generated containers is not the true desired size since not all containers were generated.
// In the old items-based mode it didn't matter because only the scrolling panel could virtualize and scrollviewer doesn't *really*
// care about desired size.
//
// In pixel-based mode we need to compute the same desired size as if we weren't virtualizing.
//
// Note: there are faster ways to do this than loop through items, but the cost isn't significant and the other possible implementations are nasty.
//
Size containerSize;
ItemCollection items = itemsControl.Items;
for (int i = (before ? 0 : pivotIndex); i < (before ? pivotIndex : items.Count); i++)
{
containerSize = ContainerSizeForItem(itemsControl, items[i], i);
if (isHorizontal)
{
stackDesiredSize.Width += containerSize.Width;
}
else
{
stackDesiredSize.Height += containerSize.Height;
}
}
return stackDesiredSize;
}
//
// Returns the index of the first item visible (even partially) in the viewport.
//
private int ComputeIndexOfFirstVisibleItem(MeasureData measureData, ItemsControl itemsControl, bool isHorizontal, out double firstItemOffset)
{
firstItemOffset = 0d; // offset of the top of the first visible child from the top of the viewport. The child always
// starts before the top of the viewport so this is always negative.
if (itemsControl != null)
{
ItemCollection items = itemsControl.Items;
int itemsCount = items.Count;
if (!IsPixelBased)
{
//
// Classic case that shipped with V1
//
// If the panel is implementing IScrollInfo then _scrollData keeps track of the
// current offset, extent, etc in logical units
//
if (IsScrolling)
{
return CoerceIndexToInteger(isHorizontal ? _scrollData._offset.X : _scrollData._offset.Y, itemsCount);
}
}
else
{
Size containerSize;
double totalSpan = 0.0; // total height or width in the stacking direction
double containerSpan = 0.0;
double viewportOffset = isHorizontal ? measureData.Viewport.X : measureData.Viewport.Y;
for (int i = 0; i < itemsCount; i++)
{
containerSize = ContainerSizeForItem(itemsControl, items[i], i);
containerSpan = isHorizontal ? containerSize.Width : containerSize.Height;
totalSpan += containerSpan;
if (totalSpan > viewportOffset)
{
// This is the first item that starts before the viewportOffset but ends after it; i is thus the index
// to the first item in the viewport.
firstItemOffset = totalSpan - containerSpan - viewportOffset;
return i;
}
}
}
}
return 0;
}
private Size ContainerSizeForItem(ItemsControl itemsControl, object item, int index)
{
UIElement temp;
return ContainerSizeForItem(itemsControl, item, index, out temp);
}
///
/// Returns the size of the container for a given item. The size can come from the container, a lookup, or a guess depending
/// on the virtualization state of the item.
///
///
///
///
/// returns the container for the item; null if the container wasn't found
///
private Size ContainerSizeForItem(ItemsControl itemsControl, object item, int index, out UIElement container)
{
Size containerSize;
container = index >= 0 ? ((ItemContainerGenerator)Generator).ContainerFromIndex(index) as UIElement : null;
if (container != null)
{
containerSize = container.DesiredSize;
}
else
{
// It's virtualized; grab the height off the item if available.
object value = itemsControl.ReadItemValue(item, _desiredSizeStorageIndex);
if (value != null)
{
containerSize = (Size)value;
}
else
{
//
// No stored container height; simply guess.
//
containerSize = new Size();
if (Orientation == Orientation.Horizontal)
{
containerSize.Width = ContainerStackingSizeEstimate(itemsControl, /*isHorizontal = */ true);
containerSize.Height = DesiredSize.Height;
}
else
{
containerSize.Height = ContainerStackingSizeEstimate(itemsControl, /*isHorizontal = */ false);
containerSize.Width = DesiredSize.Width;
}
}
}
Debug.Assert(!containerSize.IsEmpty, "We can't estimate an empty size");
return containerSize;
}
private double ContainerStackingSizeEstimate(ItemsControl itemsControl, bool isHorizontal)
{
return ContainerStackingSizeEstimate(itemsControl as IProvideStackingSize, isHorizontal);
}
///
/// Estimates a container size in the stacking direction for the given ItemsControl
///
///
///
///
private double ContainerStackingSizeEstimate(IProvideStackingSize estimate, bool isHorizontal)
{
double stackingSize = 0d;
if (estimate != null)
{
stackingSize = estimate.EstimatedContainerSize(isHorizontal);
}
if (stackingSize <= 0d || DoubleUtil.IsNaN(stackingSize))
{
stackingSize = ScrollViewer._scrollLineDelta;
}
Debug.Assert(stackingSize > 0, "We should have returned a reasonable estimate for the stacking size");
return stackingSize;
}
private MeasureData CreateChildMeasureData(MeasureData measureData, Size layoutSlotSize, Size stackDesiredSize, bool isHorizontal)
{
Invariant.Assert(IsPixelBased && measureData != null, "We can only use MeasureData when pixel-based");
Rect viewport = measureData.Viewport;
//
// Adjust viewport offset for the child
//
if (isHorizontal)
{
viewport.X -= stackDesiredSize.Width;
}
else
{
viewport.Y -= stackDesiredSize.Height;
}
return new MeasureData(layoutSlotSize, viewport);
}
///
/// Inserts a new container in the visual tree
///
///
///
private void InsertNewContainer(int childIndex, UIElement container)
{
InsertContainer(childIndex, container, false);
}
///
/// Inserts a recycled container in the visual tree
///
///
///
///
private bool InsertRecycledContainer(int childIndex, UIElement container)
{
return InsertContainer(childIndex, container, true);
}
///
/// Inserts a container into the Children collection. The container is either new or recycled.
///
///
///
///
private bool InsertContainer(int childIndex, UIElement container, bool isRecycled)
{
Debug.Assert(container != null, "Null container was generated");
bool visualOrderChanged = false;
UIElementCollection children = InternalChildren;
//
// Find the index in the Children collection where we hope to insert the container.
// This is done by looking up the index of the container BEFORE the one we hope to insert.
//
// We have to do it this way because there could be recycled containers between the container we're looking for and the one before it.
// By finding the index before the place we want to insert and adding one, we ensure that we'll insert the new container in the
// proper location.
//
// In recycling mode childIndex is the index in the _realizedChildren list, not the index in the
// Children collection. We have to convert the index; we'll call the index in the Children collection
// the visualTreeIndex.
//
int visualTreeIndex = 0;
if (childIndex > 0)
{
visualTreeIndex = ChildIndexFromRealizedIndex(childIndex - 1);
visualTreeIndex++;
}
if (isRecycled && visualTreeIndex < children.Count && children[visualTreeIndex] == container)
{
// Don't insert if a recycled container is in the proper place already
}
else
{
if (visualTreeIndex < children.Count)
{
int insertIndex = visualTreeIndex;
if (isRecycled && container.InternalVisualParent != null)
{
// If the container is recycled we have to remove it from its place in the visual tree and
// insert it in the proper location. For perf we'll use an internal Move API that moves
// the first parameter to right before the second one.
Debug.Assert(children[visualTreeIndex] != null, "MoveVisualChild interprets a null destination as 'move to end'");
children.MoveVisualChild(container, children[visualTreeIndex]);
visualOrderChanged = true;
}
else
{
VirtualizingPanel.InsertInternalChild(children, insertIndex, container);
}
}
else
{
if (isRecycled && container.InternalVisualParent != null)
{
// Recycled container is still in the tree; move it to the end
children.MoveVisualChild(container, null);
visualOrderChanged = true;
}
else
{
VirtualizingPanel.AddInternalChild(children, container);
}
}
}
//
// Keep realizedChildren in [....] w/ the visual tree.
//
if (IsVirtualizing && InRecyclingMode)
{
_realizedChildren.Insert(childIndex, container);
}
Generator.PrepareItemContainer(container);
return visualOrderChanged;
}
private void EnsureCleanupOperation(bool delay)
{
if (delay)
{
bool noPendingOperations = true;
if (_cleanupOperation != null)
{
noPendingOperations = _cleanupOperation.Abort();
if (noPendingOperations)
{
_cleanupOperation = null;
}
}
if (noPendingOperations && (_cleanupDelay == null))
{
_cleanupDelay = new DispatcherTimer();
_cleanupDelay.Tick += new EventHandler(OnDelayCleanup);
_cleanupDelay.Interval = TimeSpan.FromMilliseconds(500.0);
_cleanupDelay.Start();
}
}
else
{
if ((_cleanupOperation == null) && (_cleanupDelay == null))
{
_cleanupOperation = Dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(OnCleanUp), null);
}
}
}
private bool PreviousChildIsGenerated(int childIndex)
{
GeneratorPosition position = new GeneratorPosition(childIndex, 0);
position = Generator.GeneratorPositionFromIndex(Generator.IndexFromGeneratorPosition(position) - 1);
return (position.Offset == 0 && position.Index >= 0);
}
///
/// Takes a container returned from Generator.GenerateNext() and places it in the visual tree if necessary.
/// Takes into account whether the container is new, recycled, or already realized.
///
///
///
///
private bool AddContainerFromGenerator(int childIndex, UIElement child, bool newlyRealized)
{
bool visualOrderChanged = false;
if (!newlyRealized)
{
//
// Container is either realized or recycled. If it's realized do nothing; it already exists in the visual
// tree in the proper place.
//
if (InRecyclingMode)
{
// Note there's no check for IsVirtualizing here. If the user has just flipped off virtualization it's possible that
// the Generator will still return some recycled containers until its list runs out.
IList children = RealizedChildren;
if (childIndex >= children.Count || !(children[childIndex] == child))
{
Debug.Assert(!children.Contains(child), "we incorrectly identified a recycled container");
//
// We have a recycled container (if it was a realized container it would have been returned in the
// proper location). Note also that recycled containers are NOT in the _realizedChildren list.
//
visualOrderChanged = InsertRecycledContainer(childIndex, child);
}
else
{
// previously realized child.
}
}
else
{
// Not recycling; realized container
Debug.Assert(child == InternalChildren[childIndex], "Wrong child was generated");
}
}
else
{
InsertNewContainer(childIndex, child);
}
return visualOrderChanged;
}
private UIElement GeneratePreviousChild(int childIndex, Size layoutSlotSize)
{
int newIndex = Generator.IndexFromGeneratorPosition(new GeneratorPosition(childIndex, 0)) - 1;
if (newIndex >= 0)
{
UIElement child;
bool visualOrderChanged = false;
IItemContainerGenerator generator = Generator;
int newGeneratedIndex;
GeneratorPosition newStartPos = IndexToGeneratorPositionForStart(newIndex, out newGeneratedIndex);
using (generator.StartAt(newStartPos, GeneratorDirection.Forward, true))
{
bool newlyRealized;
child = generator.GenerateNext(out newlyRealized) as UIElement;
Debug.Assert(child != null, "Null child was generated");
AddContainerFromGenerator(childIndex, child, newlyRealized);
if (childIndex <= _firstVisibleChildIndex)
{
_firstVisibleChildIndex++;
}
child.Measure(layoutSlotSize);
}
if (visualOrderChanged)
{
Debug.Assert(IsVirtualizing && InRecyclingMode, "We should only modify the visual order when in recycling mode");
InvalidateZState();
}
return child;
}
return null;
}
private void OnItemsRemove(ItemsChangedEventArgs args)
{
RemoveChildRange(args.Position, args.ItemCount, args.ItemUICount);
}
private void OnItemsReplace(ItemsChangedEventArgs args)
{
RemoveChildRange(args.Position, args.ItemCount, args.ItemUICount);
}
private void OnItemsMove(ItemsChangedEventArgs args)
{
RemoveChildRange(args.OldPosition, args.ItemCount, args.ItemUICount);
}
private void RemoveChildRange(GeneratorPosition position, int itemCount, int itemUICount)
{
if (IsItemsHost)
{
UIElementCollection children = InternalChildren;
int pos = position.Index;
if (position.Offset > 0)
{
// An item is being removed after the one at the index
pos++;
}
if (pos < children.Count)
{
int uiCount = itemUICount;
Debug.Assert((itemCount == itemUICount) || (itemUICount == 0), "Both ItemUICount and ItemCount should be equal or ItemUICount should be 0.");
if (uiCount > 0)
{
VirtualizingPanel.RemoveInternalChildRange(children, pos, uiCount);
if (IsVirtualizing && InRecyclingMode)
{
_realizedChildren.RemoveRange(pos, uiCount);
}
}
}
}
}
private void AdjustCacheWindow(int firstViewport, int itemCount)
{
//
// Adjust the container cache window such that the viewport is always contained inside.
//
// firstViewport is the index of the first container in the viewport, not counting the before trail.
// _visibleCount is the total number of items we generated. It already contains the _afterTrail.
// First and last containers that we must keep in view; index is into the data item collection
int firstContainer = firstViewport > 0 ? firstViewport - _beforeTrail : firstViewport;
int lastContainer = firstViewport + _visibleCount - 1; // beforeTrail is not included in _visibleCount
// clamp last container
if (lastContainer >= itemCount)
{
lastContainer = itemCount - 1;
}
int cacheEnd = CacheEnd;
if (firstContainer < _cacheStart)
{
// shift the cache start up
_cacheStart = firstContainer;
}
else if (lastContainer > cacheEnd)
{
// shift the cache start down
_cacheStart += (lastContainer - cacheEnd);
}
// In some cases cacheEnd can be past the end of the list of items. This is perfectly fine.
Debug.Assert(_cacheStart <= firstContainer && (CacheEnd >= firstContainer + _visibleCount - 1 || CacheEnd >= itemCount - 1), "The container cache window is out of place");
}
private bool IsOutsideCacheWindow(int itemIndex)
{
return (itemIndex < _cacheStart || itemIndex > CacheEnd);
}
///
/// Immediately cleans up any containers that have gone offscreen. Called by MeasureOverride.
/// When recycling this runs before generating and measuring children; otherwise it runs after.
///
private void CleanupContainers(int firstViewport, ItemsControl itemsControl)
{
Debug.Assert(IsVirtualizing, "Can't clean up containers if not virtualizing");
Debug.Assert(InRecyclingMode || IsPixelBased,
"For backwards compat the standard virtualizing mode has its own cleanup algorithm");
Debug.Assert(itemsControl != null, "We can't cleanup if we aren't the itemshost");
//
// It removes items outside of the container cache window (a logical 'window' at
// least as large as the viewport).
//
// firstViewport is the index of first data item that will be in the viewport
// at the end of Measure. This is effectively the scroll offset.
//
// _visibleStart is index of the first data item that was previously at the top of the viewport
// At the end of a Measure pass _visibleStart == firstViewport.
//
// _visibleCount is the number of data items that were previously visible in the viewport.
int cleanupRangeStart = -1;
int cleanupCount = 0;
int itemIndex = -1; // data item index used to compare with the cache window position.
int lastItemIndex;
IList children = RealizedChildren;
int focusedChild = -1, previousFocusable = -1, nextFocusable = -1; // child indices for the focused item and before and after focus trail items
bool performCleanup = false;
UIElement child;
if (children.Count == 0)
{
return; // nothing to do
}
AdjustCacheWindow(firstViewport, itemsControl.Items.Count);
if (IsKeyboardFocusWithin && !IsPixelBased)
{
// If we're not in a hieararchy we can find the focus trail locally; for hierarchies it has already been
// precalculated.
FindFocusedChild(out focusedChild, out previousFocusable, out nextFocusable);
}
//
// Iterate over all realized children and recycle the ones that are eligible. Items NOT eligible for recycling
// have one or more of the following properties
//
// - inside the cache window
// - the item is its own container
// - has keyboard focus
// - is the first focusable item before or after the focused item
// - the CleanupVirtualizedItem event was canceled
//
for (int childIndex = 0; childIndex < children.Count; childIndex++)
{
child = (UIElement)children[childIndex];
lastItemIndex = itemIndex;
itemIndex = GetGeneratedIndex(childIndex);
if (itemIndex - lastItemIndex != 1)
{
// There's a generated gap between the current item and the last. Clean up the last range of items.
performCleanup = true;
}
if (performCleanup)
{
if (cleanupRangeStart >= 0 && cleanupCount > 0)
{
//
// We've hit a non-virtualizable container or a non-contiguous section.
//
CleanupRange(children, Generator, cleanupRangeStart, cleanupCount);
// CleanupRange just modified the _realizedChildren list. Adjust the childIndex.
childIndex -= cleanupCount;
focusedChild -= cleanupCount;
previousFocusable -= cleanupCount;
nextFocusable -= cleanupCount;
cleanupCount = 0;
cleanupRangeStart = -1;
}
performCleanup = false;
}
if (IsOutsideCacheWindow(itemIndex) &&
!((IGeneratorHost)itemsControl).IsItemItsOwnContainer(itemsControl.Items[itemIndex]) &&
childIndex != focusedChild &&
childIndex != previousFocusable &&
childIndex != nextFocusable &&
!IsInFocusTrail(child) && // logically the same computation as the three above; used when in a treeview.
child != _bringIntoViewContainer && // the container we're going to bring into view must not be recycled
NotifyCleanupItem(child, itemsControl))
{
//
// The container is eligible to be virtualized
//
if (cleanupRangeStart == -1)
{
cleanupRangeStart = childIndex;
}
cleanupCount++;
//
// Save off the child's desired size if we're doing pixel-based virtualization.
// We need to save off the size when doing hierarchical (i.e. TreeView) virtualization, since containers will vary
// greatly in size. This is required both to compute the index of the first visible item in the viewport and to Arrange
// children in their proper locations.
//
if (IsPixelBased)
{
itemsControl.StoreItemValue(itemsControl.Items[itemIndex], child.DesiredSize, _desiredSizeStorageIndex);
}
}
else
{
// Non-recyclable container;
performCleanup = true;
}
}
if (cleanupRangeStart >= 0 && cleanupCount > 0)
{
CleanupRange(children, Generator, cleanupRangeStart, cleanupCount);
}
}
private void EnsureRealizedChildren()
{
Debug.Assert(InRecyclingMode, "This method only applies to recycling mode");
if (_realizedChildren == null)
{
UIElementCollection children = InternalChildren;
_realizedChildren = new List(children.Count);
for (int i = 0; i < children.Count; i++)
{
_realizedChildren.Add(children[i]);
}
}
}
[Conditional("DEBUG")]
private void debug_VerifyRealizedChildren()
{
// Debug method that ensures the _realizedChildren list matches the realized containers in the Generator.
Debug.Assert(IsVirtualizing && InRecyclingMode, "Realized children only exist when recycling");
Debug.Assert(_realizedChildren != null, "Realized children must exist to verify it");
System.Windows.Controls.ItemContainerGenerator generator = Generator as System.Windows.Controls.ItemContainerGenerator;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (generator != null && itemsControl != null && itemsControl.IsGrouping == false)
{
foreach (UIElement child in InternalChildren)
{
int dataIndex = generator.IndexFromContainer(child);
if (dataIndex == -1)
{
// Child is not in the generator's realized container list (i.e. it's a recycled container): ensure it's NOT in _realizedChildren.
Debug.Assert(!_realizedChildren.Contains(child), "_realizedChildren should not contain recycled containers");
}
else
{
// Child is a realized container; ensure it's in _realizedChildren at the proper place.
GeneratorPosition position = Generator.GeneratorPositionFromIndex(dataIndex);
Debug.Assert(_realizedChildren[position.Index] == child, "_realizedChildren is corrupt!");
}
}
}
}
[Conditional("DEBUG")]
private void debug_AssertRealizedChildrenEqualVisualChildren()
{
if (IsVirtualizing && InRecyclingMode)
{
UIElementCollection children = InternalChildren;
Debug.Assert(_realizedChildren.Count == children.Count, "Realized and visual children must match");
for (int i = 0; i < children.Count; i++)
{
Debug.Assert(_realizedChildren[i] == children[i], "Realized and visual children must match");
}
}
}
///
/// Takes an index from the realized list and returns the corresponding index in the Children collection
///
///
///
private int ChildIndexFromRealizedIndex(int realizedChildIndex)
{
//
// If we're not recycling containers then we're not using a realizedChild index and no translation is necessary
//
if (IsVirtualizing && InRecyclingMode)
{
if (realizedChildIndex < _realizedChildren.Count)
{
UIElement child = _realizedChildren[realizedChildIndex];
UIElementCollection children = InternalChildren;
for (int i = realizedChildIndex; i < children.Count; i++)
{
if (children[i] == child)
{
return i;
}
}
Debug.Assert(false, "We should have found a child");
}
}
return realizedChildIndex;
}
///
/// Recycled containers still in the Children collection at the end of Measure should be disconnected
/// from the visual tree. Otherwise they're still visible to things like Arrange, keyboard navigation, etc.
///
private void DisconnectRecycledContainers()
{
int realizedIndex = 0;
UIElement visualChild;
UIElement realizedChild = _realizedChildren.Count > 0 ? _realizedChildren[0] : null;
UIElementCollection children = InternalChildren;
for (int i = 0; i < children.Count; i++)
{
visualChild = children[i];
if (visualChild == realizedChild)
{
realizedIndex++;
if (realizedIndex < _realizedChildren.Count)
{
realizedChild = _realizedChildren[realizedIndex];
}
else
{
realizedChild = null;
}
}
else
{
// The visual child is a recycled container
children.RemoveNoVerify(visualChild);
i--;
}
}
debug_VerifyRealizedChildren();
debug_AssertRealizedChildrenEqualVisualChildren();
}
private GeneratorPosition IndexToGeneratorPositionForStart(int index, out int childIndex)
{
IItemContainerGenerator generator = Generator;
GeneratorPosition position = (generator != null) ? generator.GeneratorPositionFromIndex(index) : new GeneratorPosition(-1, index + 1);
// determine the position in the children collection for the first
// generated container. This assumes that generator.StartAt will be called
// with direction=Forward and allowStartAtRealizedItem=true.
childIndex = (position.Offset == 0) ? position.Index : position.Index + 1;
return position;
}
#region Delayed Cleanup Methods
//
// Delayed Cleanup is used when the VirtualizationMode is standard (not recycling) and the panel is scrolling and item-based
// It chooses to defer virtualizing items until there are enough available. It then cleans them using a background priority dispatcher
// work item
//
private void OnDelayCleanup(object sender, EventArgs e)
{
Debug.Assert(_cleanupDelay != null);
bool needsMoreCleanup = false;
try
{
needsMoreCleanup = CleanUp();
}
finally
{
// Cleanup the timer if more cleanup is unnecessary
if (!needsMoreCleanup)
{
_cleanupDelay.Stop();
_cleanupDelay = null;
}
}
}
private object OnCleanUp(object args)
{
Debug.Assert(_cleanupOperation != null);
bool needsMoreCleanup = false;
try
{
needsMoreCleanup = CleanUp();
}
finally
{
// Keeping this non-null until here in case cleaning up causes re-entrancy
_cleanupOperation = null;
}
if (needsMoreCleanup)
{
EnsureCleanupOperation(true /* delay */);
}
return null;
}
private bool CleanUp()
{
Debug.Assert(!InRecyclingMode, "This method only applies to standard virtualization");
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (!IsVirtualizing || !IsItemsHost)
{
// Virtualization is turned off or we aren't hosting children; no need to cleanup.
return false;
}
int startMilliseconds = Environment.TickCount;
bool needsMoreCleanup = false;
UIElementCollection children = InternalChildren;
int minDesiredGenerated = MinDesiredGenerated;
int maxDesiredGenerated = MaxDesiredGenerated;
int pageSize = maxDesiredGenerated - minDesiredGenerated;
int extraChildren = children.Count - pageSize;
if (extraChildren > (pageSize * 2))
{
if ((Mouse.LeftButton == MouseButtonState.Pressed) &&
(extraChildren < 1000))
{
// An optimization for when we are dragging the mouse.
needsMoreCleanup = true;
}
else
{
bool trailingFocus = IsKeyboardFocusWithin;
bool keepForwardTrail = false;
int focusIndex = -1;
IItemContainerGenerator generator = Generator;
int cleanupRangeStart = 0;
int cleanupCount = 0;
int lastGeneratedIndex = -1;
int counterAdjust;
for (int i = 0; i < children.Count; i++)
{
// It is possible for TickCount to wrap around about every 30 days.
// If that were to occur, then this particular cleanup may not be interrupted.
// That is OK since the worst that can happen is that there is more of a stutter than normal.
int totalMilliseconds = Environment.TickCount - startMilliseconds;
if ((totalMilliseconds > 50) && (cleanupCount > 0))
{
// Cleanup has been working for 50ms already and the user might start
// noticing a lag. Stop cleaning up and release the thread for other work.
// Cleanup will continue later.
// Don't break out until after at least one item has been found to cleanup.
// Otherwise, we might end up in an infinite loop.
needsMoreCleanup = true;
break;
}
int childIndex = i;
if (trailingFocus)
{
// Focus lies somewhere within the panel, but it has not been found yet.
UIElement child = children[i];
if (child.IsKeyboardFocusWithin)
{
// Focus has been found, we can now re-virtualize items before the focus.
trailingFocus = false;
keepForwardTrail = true;
focusIndex = i;
if (i > 0)
{
// Go through the trailing items and find a focusable item to keep.
int trailIndex = i - 1;
int end = Math.Max(0, i - FocusTrail);
for (; trailIndex >= end; trailIndex--)
{
child = children[trailIndex];
if (Keyboard.IsFocusable(child))
{
trailIndex--;
break;
}
}
// The rest of the trailing items can be re-virtualized.
for (childIndex = end; childIndex <= trailIndex; childIndex++)
{
ManageCleanup(
children,
itemsControl,
generator,
childIndex,
minDesiredGenerated,
maxDesiredGenerated,
ref childIndex,
ref cleanupRangeStart,
ref cleanupCount,
ref lastGeneratedIndex,
out counterAdjust);
if (counterAdjust > 0)
{
i -= counterAdjust;
trailIndex -= counterAdjust;
}
}
if (cleanupCount > 0)
{
// Cleanup the last batch for the focused item
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
i -= cleanupCount;
cleanupCount = 0;
}
cleanupRangeStart = i + 1;
// At this point, we are caught up and should go to the next item
continue;
}
}
else if (i >= FocusTrail)
{
childIndex = i - FocusTrail;
}
else
{
continue;
}
}
if (keepForwardTrail)
{
// Find a focusable item after the focused item to keep
if (childIndex <= (focusIndex + FocusTrail))
{
UIElement child = children[childIndex];
if (Keyboard.IsFocusable(child))
{
// A focusable item was found, all items after this one can be re-virtualized
keepForwardTrail = false;
cleanupRangeStart = childIndex + 1;
cleanupCount = 0;
}
continue;
}
else
{
keepForwardTrail = false;
}
}
ManageCleanup(
children,
itemsControl,
generator,
childIndex,
minDesiredGenerated,
maxDesiredGenerated,
ref i,
ref cleanupRangeStart,
ref cleanupCount,
ref lastGeneratedIndex,
out counterAdjust);
}
if (cleanupCount > 0)
{
// Cleanup the final batch
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
}
}
}
return needsMoreCleanup;
}
private void ManageCleanup(
UIElementCollection children,
ItemsControl itemsControl,
IItemContainerGenerator generator,
int childIndex,
int minDesiredGenerated,
int maxDesiredGenerated,
ref int counter,
ref int cleanupRangeStart,
ref int cleanupCount,
ref int lastGeneratedIndex,
out int counterAdjust)
{
counterAdjust = 0;
bool performCleanup = false;
bool countThisChild = false;
int generatedIndex = GetGeneratedIndex(childIndex);
if (OutsideMinMax(generatedIndex, minDesiredGenerated, maxDesiredGenerated) &&
NotifyCleanupItem(childIndex, children, itemsControl))
{
// The item can be re-virtualized.
if ((generatedIndex - lastGeneratedIndex) == 1)
{
// Add another to the current batch.
cleanupCount++;
}
else
{
// There was a gap in generated items. Cleanup any from the previous batch.
performCleanup = countThisChild = true;
}
}
else
{
// The item cannot be re-virtualized. Cleanup any from the previous batch.
performCleanup = true;
}
if (performCleanup)
{
// Cleanup a batch of items
if (cleanupCount > 0)
{
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
counterAdjust = cleanupCount;
counter -= counterAdjust;
childIndex -= counterAdjust;
cleanupCount = 0;
}
if (countThisChild)
{
// The current child was not included in the batch and should be saved for later
cleanupRangeStart = childIndex;
cleanupCount = 1;
}
else
{
// The next child will start the next batch.
cleanupRangeStart = childIndex + 1;
}
}
lastGeneratedIndex = generatedIndex;
}
private bool NotifyCleanupItem(int childIndex, UIElementCollection children, ItemsControl itemsControl)
{
return NotifyCleanupItem(children[childIndex], itemsControl);
}
private bool NotifyCleanupItem(UIElement child, ItemsControl itemsControl)
{
CleanUpVirtualizedItemEventArgs e = new CleanUpVirtualizedItemEventArgs(itemsControl.ItemContainerGenerator.ItemFromContainer(child), child);
e.Source = this;
OnCleanUpVirtualizedItem(e);
return !e.Cancel;
}
private void CleanupRange(IList children, IItemContainerGenerator generator, int startIndex, int count)
{
if (InRecyclingMode)
{
Debug.Assert(startIndex >= 0 && count > 0);
Debug.Assert(children == _realizedChildren, "the given child list must be the _realizedChildren list when recycling");
((IRecyclingItemContainerGenerator)generator).Recycle(new GeneratorPosition(startIndex, 0), count);
// The call to Recycle has caused the ItemContainerGenerator to remove some items
// from its list of realized items; we adjust _realizedChildren to match.
_realizedChildren.RemoveRange(startIndex, count);
}
else
{
// Remove the desired range of children
VirtualizingPanel.RemoveInternalChildRange((UIElementCollection)children, startIndex, count);
generator.Remove(new GeneratorPosition(startIndex, 0), count);
}
AdjustFirstVisibleChildIndex(startIndex, count);
}
#endregion
///
/// Called after 'count' items were removed or recycled from the Generator. _firstVisibleChildIndex is the
/// index of the first visible container. This index isn't exactly the child position in the UIElement collection;
/// it's actually the index of the realized container inside the generator. Since we've just removed some realized
/// containers from the generator (by calling Remove or Recycle), we have to adjust the first visible child index.
///
/// index of the first removed item
/// number of items removed
private void AdjustFirstVisibleChildIndex(int startIndex, int count)
{
// Update the index of the first visible generated child
if (startIndex < _firstVisibleChildIndex)
{
int endIndex = startIndex + count - 1;
if (endIndex < _firstVisibleChildIndex)
{
// The first visible index is after the items that were removed
_firstVisibleChildIndex -= count;
}
else
{
// The first visible index was within the items that were removed
_firstVisibleChildIndex = startIndex;
}
}
}
private static bool OutsideMinMax(int i, int min, int max)
{
return ((i < min) || (i > max));
}
private void EnsureTopCapGenerated(Size layoutSlotSize)
{
// Ensure that a focusable item is generated above the first visible item
// so that keyboard navigation works.
IList children;
_beforeTrail = 0;
if (_visibleStart > 0)
{
children = RealizedChildren;
int childIndex = _firstVisibleChildIndex;
UIElement child;
// At most, we will search FocusTrail number of items for a focusable item
for (; _beforeTrail < FocusTrail; _beforeTrail++)
{
if (PreviousChildIsGenerated(childIndex))
{
// The previous child is already generated, check its focusability
childIndex--;
child = (UIElement)children[childIndex];
}
else
{
// Generate the previous child
child = GeneratePreviousChild(childIndex, layoutSlotSize);
}
if ((child == null) || Keyboard.IsFocusable(child))
{
// Either a focusable item was found, or no child was generated
_beforeTrail++;
break;
}
}
}
}
///
/// Returns the MeasureData we'll be using for computations in MeasureOverride. This updates the viewport offset
/// based on the one set in the MeasureData property prior to the call to MeasureOverride.
///
///
///
///
///
private MeasureData AdjustViewportOffset(MeasureData givenMeasureData, ItemsControl itemsControl, bool isHorizontal)
{
// Note that a panel should not modify its own MeasureData -- it needs to be treated exactly as if it was a variable
// passed into MeasureOverride. That's why we make a copy of MeasureData in this method and return that.
Rect viewport;
MeasureData newMeasureData = null;
IProvideStackingSize stackingSize;
double offset = 0d;
Debug.Assert(MeasureData == null || IsPixelBased, "If a panel has measure data then it must be pixel based");
Debug.Assert(!IsScrolling && IsPixelBased, "This only applies to internal panels");
//
// This panel isn't a scroll owner but some panel above it is. It will be able to use the viewport data
// to virtualize.
//
if (givenMeasureData != null)
{
viewport = givenMeasureData.Viewport;
stackingSize = itemsControl as IProvideStackingSize;
Debug.Assert(givenMeasureData.HasViewport, "MeasureData is only set on objects when we want to pass down viewport information.");
//
// We need to offset the viewport to take into account the delta between the top of the items control
// and this panel (i.e. the header). Ask for the header, and, if not available, use the estimated container size.
if (stackingSize != null)
{
offset = stackingSize.HeaderSize(isHorizontal);
if (offset <= 0d || DoubleUtil.IsNaN(offset))
{
offset = ContainerStackingSizeEstimate(stackingSize, isHorizontal);
}
}
if (isHorizontal)
{
viewport.X -= offset;
}
else
{
// adjust viewport for the header of the TreeViewItem containing this as an ItemsPanel.
viewport.Y -= offset;
}
newMeasureData = new MeasureData(givenMeasureData.AvailableSize, viewport);
}
return newMeasureData;
}
///
/// Sets up IsVirtualizing, VirtualizationMode, and IsPixelBased
///
/// IsVirtualizing is true if turned on via the items control and if the panel has a viewport.
/// VSP has a viewport if it's either the scrolling panel or it was given MeasureData.
///
/// IsPixelBased is true if the panel is virtualizing and (for backwards compat) is the ItemsHost for a TreeView or TreeViewItem.
/// VSP can only make use of, create, and propagate down MeasureData if it is pixel-based, since the viewport is in pixels.
///
///
private void SetVirtualizationState(ItemsControl itemsControl, bool hasMeasureData)
{
VirtualizationMode mode = (itemsControl != null) ? GetVirtualizationMode(itemsControl) : VirtualizationMode.Standard;
if (itemsControl != null)
{
// Set IsVirtualizing. This panel can only virtualize if IsVirtualizing is set on its ItemsControl and it has viewport data.
// It has viewport data if it's either the scroll host or was given viewport information by measureData.
if (GetIsVirtualizing(itemsControl) && (IsScrolling || hasMeasureData))
{
IsVirtualizing = true;
}
}
else
{
IsVirtualizing = false;
}
//
// Set up info on first measure
//
if (HasMeasured)
{
VirtualizationMode oldMode = VirtualizationMode;
if (oldMode != mode)
{
throw new InvalidOperationException(SR.Get(SRID.CantSwitchVirtualizationModePostMeasure));
}
}
else
{
HasMeasured = true;
if (IsVirtualizing && (itemsControl is TreeView || itemsControl is TreeViewItem))
{
IsPixelBased = true;
}
VirtualizationMode = mode;
}
}
private int MinDesiredGenerated
{
get
{
return Math.Max(0, _visibleStart - _beforeTrail);
}
}
private int MaxDesiredGenerated
{
get
{
return Math.Min(ItemCount, _visibleStart + _visibleCount + _afterTrail);
}
}
private int ItemCount
{
get
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
return (itemsControl != null) ? itemsControl.Items.Count : 0;
}
}
#endregion
private void EnsureScrollData()
{
if (_scrollData == null) { _scrollData = new ScrollData(); }
}
private static void ResetScrolling(VirtualizingStackPanel element)
{
element.InvalidateMeasure();
// Clear scrolling data. Because of thrash (being disconnected & reconnected, &c...), we may
if (element.IsScrolling)
{
element._scrollData.ClearLayout();
}
}
// OnScrollChange is an override called whenever the IScrollInfo exposed scrolling state changes on this element.
// At the time this method is called, scrolling state is in its new, valid state.
private void OnScrollChange()
{
if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); }
}
private void SetAndVerifyScrollingData(Size viewport, Size extent, Vector offset)
{
Debug.Assert(IsScrolling);
if (IsPixelBased)
{
// _scrollData is in pixels and thus operations like LineDown can push the offset too far.
// The behavior here is effectively the same as ScrollContentPresenter.VerifyScrollData
offset.X = ScrollContentPresenter.CoerceOffset(offset.X, extent.Width, viewport.Width);
offset.Y = ScrollContentPresenter.CoerceOffset(offset.Y, extent.Height, viewport.Height);
}
// Detect changes to the viewport, extent, and offset
bool viewportChanged = !DoubleUtil.AreClose(viewport, _scrollData._viewport);
bool extentChanged = !DoubleUtil.AreClose(extent, _scrollData._extent);
bool offsetChanged = !DoubleUtil.AreClose(offset, _scrollData._computedOffset);
// Update data and fire scroll change notifications
_scrollData._offset = offset;
if (viewportChanged || extentChanged || offsetChanged)
{
Vector oldViewportOffset = _scrollData._computedOffset;
Size oldViewportSize = _scrollData._viewport;
_scrollData._viewport = viewport;
_scrollData._extent = extent;
_scrollData._computedOffset = offset;
// Report changes to the viewport
if (viewportChanged)
{
OnViewportSizeChanged(oldViewportSize, viewport);
}
// Report changes to the offset
if (offsetChanged)
{
OnViewportOffsetChanged(oldViewportOffset, offset);
}
OnScrollChange();
}
}
///
/// Allows subclasses to be notified of changes to the viewport size data.
///
/// The old value of the size.
/// The new value of the size.
protected virtual void OnViewportSizeChanged(Size oldViewportSize, Size newViewportSize)
{
}
///
/// Allows subclasses to be notified of changes to the viewport offset data.
///
/// The old value of the offset.
/// The new value of the offset.
protected virtual void OnViewportOffsetChanged(Vector oldViewportOffset, Vector newViewportOffset)
{
}
// Translates a logical (child index) offset to a physical (1/96") when scrolling.
// If virtualizing, it makes the assumption that the logicalOffset is always the first in the visual collection
// and thus returns 0.
// If not virtualizing, it assumes that children are Measure clean; should only be called after running Measure.
private double ComputePhysicalFromLogicalOffset(double logicalOffset, bool fHorizontal)
{
double physicalOffset = 0.0;
IList children = RealizedChildren;
Debug.Assert(logicalOffset == 0 || (logicalOffset > 0 && logicalOffset < children.Count));
for (int i = 0; i < logicalOffset; i++)
{
UIElement child = (UIElement)children[i];
physicalOffset -= (fHorizontal)
? child.DesiredSize.Width
: child.DesiredSize.Height;
}
return physicalOffset;
}
private int FindChildIndexThatParentsVisual(Visual v)
{
DependencyObject child = v;
DependencyObject parent = VisualTreeHelper.GetParent(child);
while (parent != this)
{
child = parent;
parent = VisualTreeHelper.GetParent(child);
}
IList children = RealizedChildren;
for (int i = 0; i < children.Count; i++)
{
if (children[i] == child)
{
return GetGeneratedIndex(i);
}
}
return -1;
}
// This is very similar to the work that ScrollContentPresenter does for MakeVisible. Simply adjust by a
// pixel offset.
private void MakeVisiblePhysicalHelper(Rect r, ref Vector newOffset, ref Rect newRect, bool isHorizontal)
{
double viewportOffset;
double viewportSize;
double targetRectOffset;
double targetRectSize;
double minPhysicalOffset;
if (isHorizontal)
{
viewportOffset = _scrollData._computedOffset.X;
viewportSize = ViewportWidth;
targetRectOffset = r.X;
targetRectSize = r.Width;
}
else
{
viewportOffset = _scrollData._computedOffset.Y;
viewportSize = ViewportHeight;
targetRectOffset = r.Y;
targetRectSize = r.Height;
}
targetRectOffset += viewportOffset;
minPhysicalOffset = ScrollContentPresenter.ComputeScrollOffsetWithMinimalScroll(
viewportOffset, viewportOffset + viewportSize, targetRectOffset, targetRectOffset + targetRectSize);
// Compute the visible rectangle of the child relative to the viewport.
double left = Math.Max(targetRectOffset, minPhysicalOffset);
targetRectSize = Math.Max(Math.Min(targetRectSize + targetRectOffset, minPhysicalOffset + viewportSize) - left, 0);
targetRectOffset = left;
targetRectOffset -= viewportOffset;
if (isHorizontal)
{
newOffset.X = minPhysicalOffset;
newRect.X = targetRectOffset;
newRect.Width = targetRectSize;
}
else
{
newOffset.Y = minPhysicalOffset;
newRect.Y = targetRectOffset;
newRect.Height = targetRectSize;
}
}
private void MakeVisibleLogicalHelper(int childIndex, Rect r, ref Vector newOffset, ref Rect newRect)
{
bool fHorizontal = (Orientation == Orientation.Horizontal);
int firstChildInView;
int newFirstChild;
int viewportSize;
double childOffsetWithinViewport = r.Y;
if (fHorizontal)
{
firstChildInView = (int)_scrollData._computedOffset.X;
viewportSize = (int)_scrollData._viewport.Width;
}
else
{
firstChildInView = (int)_scrollData._computedOffset.Y;
viewportSize = (int)_scrollData._viewport.Height;
}
newFirstChild = firstChildInView;
// If the target child is before the current viewport, move the viewport to put the child at the top.
if (childIndex < firstChildInView)
{
childOffsetWithinViewport = 0;
newFirstChild = childIndex;
}
// If the target child is after the current viewport, move the viewport to put the child at the bottom.
else if (childIndex > firstChildInView + viewportSize - 1)
{
newFirstChild = childIndex - viewportSize + 1;
double pixelSize = fHorizontal ? ActualWidth : ActualHeight;
childOffsetWithinViewport = pixelSize * (1.0 - (1.0 / viewportSize));
}
if (fHorizontal)
{
newOffset.X = newFirstChild;
newRect.X = childOffsetWithinViewport;
newRect.Width = r.Width;
}
else
{
newOffset.Y = newFirstChild;
newRect.Y = childOffsetWithinViewport;
newRect.Height = r.Height;
}
}
// Converts an index into the item collection as a double into an int
static private int CoerceIndexToInteger(double index, int numberOfItems)
{
int newIndex;
if (Double.IsNegativeInfinity(index))
{
newIndex = 0;
}
else if (Double.IsPositiveInfinity(index))
{
newIndex = numberOfItems - 1;
}
else
{
newIndex = (int)index;
newIndex = Math.Max(Math.Min(numberOfItems - 1, newIndex), 0);
}
return newIndex;
}
private int GetGeneratedIndex(int childIndex)
{
return Generator.IndexFromGeneratorPosition(new GeneratorPosition(childIndex, 0));
}
//
// Focus Helpers
//
#region Focus Helpers
//
// Methods to keep track of focus.
//
// Dealing with Focus while virtualizing a list is easy: don't throw away the focused item and the next and previous
// focusable items. When in a TreeView it's much harder; Measure (and thus the cleanup code) for any VSP in the hierarchy
// can run at any time. The only performant way for a panel to know that one of its children may be the next or previous focusable
// item is for it to be marked. We do this every time focus changes within the hierarchy.
//
private WeakReference[] EnsureFocusTrail()
{
WeakReference[] focusTrail = FocusTrailField.GetValue(this);
if (focusTrail == null)
{
focusTrail = new WeakReference[2];
FocusTrailField.SetValue(this, focusTrail);
}
return focusTrail;
}
///
/// Finds the focused child along with the previous and next focusable children. Used only when recycling containers;
/// the standard mode has a different cleanup algorithm
///
///
///
///
private void FindFocusedChild(out int focusedChild, out int previousFocusable, out int nextFocusable)
{
Debug.Assert(InRecyclingMode, "This method is only valid for the recycling mode");
Debug.Assert(IsKeyboardFocusWithin, "we should only search for a focusable child if we have focus");
focusedChild = previousFocusable = nextFocusable = -1;
UIElement child;
bool foundFocusedChild = false;
for (int i = 0; i < _realizedChildren.Count; i++)
{
child = _realizedChildren[i];
if (!foundFocusedChild && child.IsKeyboardFocusWithin)
{
focusedChild = i;
foundFocusedChild = true;
// Go through the trailing items.
// Go through the trailing items and find a focusable item to keep.
int trailIndex = i - 1;
int end = Math.Max(0, i - FocusTrail);
for (; trailIndex >= end; trailIndex--)
{
child = _realizedChildren[trailIndex];
if (Keyboard.IsFocusable(child))
{
previousFocusable = trailIndex;
break;
}
}
}
else if (foundFocusedChild)
{
if (i <= focusedChild + FocusTrail)
{
if (Keyboard.IsFocusable(child))
{
nextFocusable = i;
break;
}
}
else
{
break;
}
}
}
}
///
/// Called when the focused item has changed. Used to set a special DP on the next and previous focusable items.
/// Only used when virtualizing in a hieararchy (i.e. TreeView virtualization).
///
///
private void FocusChanged(KeyboardFocusChangedEventArgs e)
{
if (IsVirtualizing && IsScrolling && IsPixelBased)
{
// IsScrolling ensures that only the top-level panel tracks focus.
// The IsPixelBased condition here needs explanation. It's used here to mean 'Is this panel in a hierarchy?'
// The assert below is just a reminder to modify this code if the meaning changes.
Debug.Assert(ItemsControl.GetItemsOwner(this) is TreeView);
// This code is TreeViewItem-specific, since it has its own focus logic and we can't override UIElement.PredictFocus
TreeViewItem focusedElement = Keyboard.FocusedElement as TreeViewItem;
WeakReference[] focusTrail = EnsureFocusTrail();
//
// Clear the old focus trail items
//
for (int i = 0; i < 2; i++)
{
DependencyObject trailItem = (DependencyObject)(focusTrail[i] != null ? focusTrail[i].Target : null);
if (trailItem != null)
{
FocusTrailItemField.ClearValue(trailItem);
}
}
//
// Set the new focus trail items
//
if (IsKeyboardFocusWithin)
{
DependencyObject previous = null;
DependencyObject next = null;
if (focusedElement != null)
{
if (Orientation == Orientation.Horizontal)
{
previous = focusedElement.InternalPredictFocus(FocusNavigationDirection.Left);
next = focusedElement.InternalPredictFocus(FocusNavigationDirection.Right);
}
else
{
previous = focusedElement.InternalPredictFocus(FocusNavigationDirection.Up);
next = focusedElement.InternalPredictFocus(FocusNavigationDirection.Down);
}
}
if (previous != null)
{
FocusTrailItemField.SetValue(previous, true);
focusTrail[0] = new WeakReference(previous);
}
if (next != null)
{
FocusTrailItemField.SetValue(next, true);
focusTrail[1] = new WeakReference(next);
}
}
else
{
// Focus has left the tree
FocusTrailField.SetValue(this, null);
}
}
}
///
/// Checks the precomputed focus trail. Valid only if we're in a hierararchy.
///
///
///
private bool IsInFocusTrail(UIElement container)
{
if (IsPixelBased)
{
return FocusTrailItemField.GetValue(container) || container.IsKeyboardFocusWithin;
}
else
{
return false;
}
}
#endregion
//------------------------------------------------------------
// Avalon Property Callbacks/Overrides
//-----------------------------------------------------------
#region Avalon Property Callbacks/Overrides
///
///
///
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Since Orientation is so essential to logical scrolling/virtualization, we synchronously check if
// the new value is different and clear all scrolling data if so.
ResetScrolling(d as VirtualizingStackPanel);
}
#endregion
#endregion Private Methods
//-----------------------------------------------------
//
// Private Properties
//
//-----------------------------------------------------
#region Private Properties
///
/// Index of the last item in the cache window
///
private int CacheEnd
{
get
{
// Note we don't have the _afterTrail here: _afterTrail is already contained inside of _visibleCount.
int cacheCount = _beforeTrail + _visibleCount + ContainerCacheSize;
if (cacheCount > 0)
{
return _cacheStart + cacheCount - 1;
}
else
{
return 0;
}
}
}
///
/// True after the first MeasureOverride call. We can't use UIElement.NeverMeasured because it's set to true by the first call to MeasureOverride.
/// Stored in a bool field on Panel.
///
private bool HasMeasured
{
get
{
return VSP_HasMeasured;
}
set
{
VSP_HasMeasured = value;
}
}
private bool InRecyclingMode
{
get
{
return _virtualizationMode == VirtualizationMode.Recycling;
}
}
internal bool IsScrolling
{
get { return (_scrollData != null) && (_scrollData._scrollOwner != null); }
}
///
/// Specifies if this panel uses item-based or pixel-based computations in Measure and Arrange.
///
/// Differences between the two:
///
/// When pixel-based mode VSP behaves the same to the layout system virtualized as not; its desired size is the sum
/// of all its children and it arranges children such that the ones in view appear in the right place.
/// In this mode VSP is also able to make use of the viewport passed down in MeasureData to virtualize chidren. When
/// it's the scrolling panel it computes the offset and extent in pixels rather than logical units.
///
/// When in item mode VSP's desired size grows and shrinks depending on which containers are virtualized and it arranges
/// all children one on top the the other.
/// In this mode VSP cannot use the viewport from MeasureData to virtualize; it can only virtualize if it is the scrolling panel
/// (IsScrolling == true). Thus its looseness with desired size isn't much of an issue since it owns the extent.
///
///
/// This should be private, except that one Debug.Assert in TreeView requires it.
///
internal bool IsPixelBased
{
get
{
// For backwards compat we don't use pixel mode unless we're virtualzing a TreeView or TreeViewItem. This should
// be changed if we decide to later publicly expose the pixel-based viewport.
Debug.Assert(VSP_IsPixelBased == false || IsVirtualizing && (ItemsControl.GetItemsOwner(this) is TreeView || ItemsControl.GetItemsOwner(this) is TreeViewItem));
return VSP_IsPixelBased;
}
set
{
VSP_IsPixelBased = value;
}
}
private bool IsVirtualizing
{
get
{
return VSP_IsVirtualizing;
}
set
{
// We must be the ItemsHost to turn on Virtualization.
bool isVirtualizing = IsItemsHost && value;
if (isVirtualizing == false)
{
_realizedChildren = null;
}
VSP_IsVirtualizing = value;
}
}
///
/// Returns the list of childen that have been realized by the Generator.
/// We must use this method whenever we interact with the Generator's index.
/// In recycling mode the Children collection also contains recycled containers and thus does
/// not map to the Generator's list.
///
private IList RealizedChildren
{
get
{
if (IsVirtualizing && InRecyclingMode)
{
EnsureRealizedChildren();
return _realizedChildren;
}
else
{
return InternalChildren;
}
}
}
private VirtualizationMode VirtualizationMode
{
get
{
return _virtualizationMode;
}
set
{
_virtualizationMode = value;
}
}
#endregion Private Properties
//------------------------------------------------------
//
// Private Fields
//
//-----------------------------------------------------
#region Private Fields
// Scrolling and virtualization data. Only used when this is the scrolling panel (IsScrolling is true).
// When VSP is in pixel mode _scrollData is in units of pixels. Otherwise the units are logical.
private ScrollData _scrollData;
// Virtualization state
private VirtualizationMode _virtualizationMode;
private int _visibleStart; // index of of the first visible data item
private int _visibleCount; // count of the number of data items visible in the viewport
private int _cacheStart; // index of the first data item in the container cache. This is always <= _visibleStart
// UIElement collection index of the first visible child container. This is NOT the data item index. If the first visible container
// is the 3rd child in the visual tree and contains data item 312, _firstVisibleChildIndex will be 2, while _visibleStart is 312.
// This is useful because could be several live containers in the collection offscreen (maybe we cleaned up lazily, they couldn't be virtualized, etc).
// This actually maps directly to realized containers inside the Generator. It's the index of the first visible realized container.
// Note that when RecyclingMode is active this is the index into the _realizedChildren collection, not the Children collection.
private int _firstVisibleChildIndex;
// Used by the Recycling mode to maintain the list of actual realized children (a realized child is one that the ItemContainerGenerator has
// generated). We need a mapping between children in the UIElementCollection and realized containers in the generator. In standard virtualization
// mode these lists are identical; in recycling mode they are not. When a container is recycled the Generator removes it from its realized list, but
// for perf reasons the panel keeps these containers in its UIElement collection. This list is the actual realized children -- i.e. the InternalChildren
// list minus all recycled containers.
private List _realizedChildren;
// Cleanup
private DispatcherOperation _cleanupOperation;
private DispatcherTimer _cleanupDelay;
private int _beforeTrail = 0;
private int _afterTrail = 0;
private const int FocusTrail = 5; // The maximum number of items off the edge we will generate to get a focused item (so that keyboard navigation can work)
private DependencyObject _bringIntoViewContainer; // pointer to the container we're about to bring into view; it can't be recycled even if it's offscreen.
// ContainerCacheSize specifies how many items we cache past the viewport boundaries. Until we expose an API to allow users to tweak this
// the safest thing is to leave it at 0.
private const int ContainerCacheSize = 0;
// Global index used by ItemValueStorage to store the DesiredSize of a UIElement when it is a virtualized container.
// Used by TreeView and TreeViewItem to remember the size of TreeViewItems when they get virtualized away.
private static int _desiredSizeStorageIndex;
// Holds the 'focus trail': the previous or next focusable item, neither of which can be virtualized.
// Used only when virtualizing in a hierarchy (i.e. TreeView virtualization).
private static UncommonField FocusTrailField = new UncommonField(null);
private static UncommonField FocusTrailItemField = new UncommonField(false);
#endregion Private Fields
//------------------------------------------------------
//
// Private Structures / Classes
//
//------------------------------------------------------
#region Private Structures Classes
//-----------------------------------------------------------
// ScrollData class
//------------------------------------------------------------
#region ScrollData
// Helper class to hold scrolling data.
// This class exists to reduce working set when VirtualizingStackPanel is used outside a scrolling situation.
// Standard "extra pointer always for less data sometimes" cache savings model:
// !Scroll [1xReference]
// Scroll [1xReference] + [6xDouble + 1xReference]
private class ScrollData
{
// Clears layout generated data.
// Does not clear scrollOwner, because unless resetting due to a scrollOwner change, we won't get reattached.
internal void ClearLayout()
{
_offset = new Vector();
_viewport = _extent = _maxDesiredSize = new Size();
}
// For Stack/Flow, the two dimensions of properties are in different units:
// 1. The "logically scrolling" dimension uses items as units.
// 2. The other dimension physically scrolls. Units are in Avalon pixels (1/96").
internal bool _allowHorizontal;
internal bool _allowVertical;
// Scroll offset of content. Positive corresponds to a visually upward offset. Set by methods like LineUp, PageDown, etc.
internal Vector _offset;
// Computed offset based on _offset set by the IScrollInfo methods. Set at the end of a successful Measure pass.
// This is the offset used by Arrange and exposed externally. Thus an offset set by PageDown via IScrollInfo isn't
// reflected publicly (e.g. via the VerticalOffset property) until a Measure pass.
internal Vector _computedOffset = new Vector(0,0);
internal Size _viewport; // ViewportSize is in {pixels x items} (or vice-versa).
internal Size _extent; // Extent is the total number of children (logical dimension) or physical size
internal ScrollViewer _scrollOwner; // ScrollViewer to which we're attached.
internal Size _maxDesiredSize; // Hold onto the maximum desired size to avoid re-laying out the parent ScrollViewer.
}
#endregion ScrollData
///
/// Allows pixel-based virtualization to ask an ItemsControl for the size of its header (if available)
/// and a size estimate for its containers. This is used for TreeView virtualization.
///
///
internal interface IProvideStackingSize
{
double HeaderSize(bool isHorizontal);
double EstimatedContainerSize(bool isHorizontal);
}
#endregion Private Structures Classes
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
//----------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation. All rights reserved.
//
//---------------------------------------------------------------------------
//#define Profiling
using MS.Internal;
using MS.Internal.Controls;
using MS.Utility;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using System.Windows.Input;
namespace System.Windows.Controls
{
///
/// VirtualizingStackPanel is used to arrange children into single line.
///
public class VirtualizingStackPanel : VirtualizingPanel, IScrollInfo
{
//-------------------------------------------------------------------
//
// Constructors
//
//-------------------------------------------------------------------
#region Constructors
///
/// Default constructor.
///
public VirtualizingStackPanel()
{
}
static VirtualizingStackPanel()
{
lock (DependencyProperty.Synchronized)
{
_desiredSizeStorageIndex = DependencyProperty.GetUniqueGlobalIndex(null, null);
DependencyProperty.RegisteredPropertyList.Add();
}
}
#endregion Constructors
//--------------------------------------------------------------------
//
// Public Methods
//
//-------------------------------------------------------------------
#region Public Methods
//------------------------------------------------------------
// IScrollInfo Methods
//------------------------------------------------------------
#region IScrollInfo Methods
///
/// Scroll content by one line to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineUp()
{
SetVerticalOffset(VerticalOffset - ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineDown()
{
SetVerticalOffset(VerticalOffset + ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one line to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "line" means.
///
public virtual void LineRight()
{
SetHorizontalOffset(HorizontalOffset + ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageUp()
{
SetVerticalOffset(VerticalOffset - ViewportHeight);
}
///
/// Scroll content by one page to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageDown()
{
SetVerticalOffset(VerticalOffset + ViewportHeight);
}
///
/// Scroll content by one page to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageLeft()
{
SetHorizontalOffset(HorizontalOffset - ViewportWidth);
}
///
/// Scroll content by one page to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of what "page" means.
///
public virtual void PageRight()
{
SetHorizontalOffset(HorizontalOffset + ViewportWidth);
}
///
/// Scroll content by one page to the top.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - SystemParameters.WheelScrollLines * ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the bottom.
/// Subclases can override this method and call SetVerticalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + SystemParameters.WheelScrollLines * ((Orientation == Orientation.Vertical && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the left.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelLeft()
{
SetHorizontalOffset(HorizontalOffset - 3.0 * ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Scroll content by one page to the right.
/// Subclases can override this method and call SetHorizontalOffset to change
/// the behavior of the mouse wheel increment.
///
public virtual void MouseWheelRight()
{
SetHorizontalOffset(HorizontalOffset + 3.0 * ((Orientation == Orientation.Horizontal && !IsPixelBased) ? 1.0 : ScrollViewer._scrollLineDelta));
}
///
/// Set the HorizontalOffset to the passed value.
///
public void SetHorizontalOffset(double offset)
{
EnsureScrollData();
double scrollX = ScrollContentPresenter.ValidateInputOffset(offset, "HorizontalOffset");
if (!DoubleUtil.AreClose(scrollX, _scrollData._offset.X))
{
Vector oldViewportOffset = _scrollData._offset;
// Store the new offset
_scrollData._offset.X = scrollX;
// Report the change in offset
OnViewportOffsetChanged(oldViewportOffset, _scrollData._offset);
InvalidateMeasure();
}
}
///
/// Set the VerticalOffset to the passed value.
///
public void SetVerticalOffset(double offset)
{
EnsureScrollData();
double scrollY = ScrollContentPresenter.ValidateInputOffset(offset, "VerticalOffset");
if (!DoubleUtil.AreClose(scrollY, _scrollData._offset.Y))
{
Vector oldViewportOffset = _scrollData._offset;
// Store the new offset
_scrollData._offset.Y = scrollY;
// Report the change in offset
OnViewportOffsetChanged(oldViewportOffset, _scrollData._offset);
InvalidateMeasure();
}
}
///
/// VirtualizingStackPanel implementation of .
///
// The goal is to change offsets to bring the child into view, and return a rectangle in our space to make visible.
// The rectangle we return is in the physical dimension the input target rect transformed into our pace.
// In the logical dimension, it is our immediate child's rect.
// Note: This code presently assumes we/children are layout clean. See work item 22269 for more detail.
public Rect MakeVisible(Visual visual, Rect rectangle)
{
Vector newOffset = new Vector();
Rect newRect = new Rect();
Rect originalRect = rectangle;
bool isHorizontal = (Orientation == Orientation.Horizontal);
// We can only work on visuals that are us or children.
// An empty rect has no size or position. We can't meaningfully use it.
if ( rectangle.IsEmpty
|| visual == null
|| visual == (Visual)this
|| !this.IsAncestorOf(visual))
{
return Rect.Empty;
}
#pragma warning disable 1634, 1691
#pragma warning disable 56506
// Compute the child's rect relative to (0,0) in our coordinate space.
// This is a false positive by PreSharp. visual cannot be null because of the 'if' check above
GeneralTransform childTransform = visual.TransformToAncestor(this);
#pragma warning restore 56506
#pragma warning restore 1634, 1691
rectangle = childTransform.TransformBounds(rectangle);
// We can't do any work unless we're scrolling.
if (!IsScrolling)
{
return rectangle;
}
// Make ourselves visible in the non-stacking direction
MakeVisiblePhysicalHelper(rectangle, ref newOffset, ref newRect, !isHorizontal);
if (IsPixelBased)
{
MakeVisiblePhysicalHelper(rectangle, ref newOffset, ref newRect, isHorizontal);
}
else
{
// Bring our child containing the visual into view.
// For non-pixel based scrolling the offset is in logical units in the stacking direction
// and physical units in the other. Hence the logical helper call here.
int childIndex = FindChildIndexThatParentsVisual(visual);
MakeVisibleLogicalHelper(childIndex, rectangle, ref newOffset, ref newRect);
}
// We have computed the scrolling offsets; validate and scroll to them.
newOffset.X = ScrollContentPresenter.CoerceOffset(newOffset.X, _scrollData._extent.Width, _scrollData._viewport.Width);
newOffset.Y = ScrollContentPresenter.CoerceOffset(newOffset.Y, _scrollData._extent.Height, _scrollData._viewport.Height);
if (!DoubleUtil.AreClose(newOffset, _scrollData._offset))
{
Vector oldOffset = _scrollData._offset;
_scrollData._offset = newOffset;
OnViewportOffsetChanged(oldOffset, newOffset);
InvalidateMeasure();
OnScrollChange();
if (ScrollOwner != null)
{
// When layout gets updated it may happen that visual is obscured by a ScrollBar
// We call MakeVisible again to make sure element is visible in this case
ScrollOwner.MakeVisible(visual, originalRect);
}
_bringIntoViewContainer = null;
}
// Return the rectangle
return newRect;
}
///
/// Generates the item at the specified index and calls BringIntoView on it.
///
/// Specify the item index that should become visible
///
/// Thrown if index is out of range
///
protected internal override void BringIndexIntoView(int index)
{
if (index < 0 || index >= ItemCount)
throw new ArgumentOutOfRangeException("index");
IItemContainerGenerator generator = Generator;
int childIndex;
bool visualOrderChanged = false;
GeneratorPosition position = IndexToGeneratorPositionForStart(index, out childIndex);
using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
bool newlyRealized;
UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
if (child != null)
{
visualOrderChanged = AddContainerFromGenerator(childIndex, child, newlyRealized);
if (visualOrderChanged)
{
Debug.Assert(IsVirtualizing && InRecyclingMode, "We should only modify the visual order when in recycling mode");
InvalidateZState();
}
FrameworkElement element = child as FrameworkElement;
if (element != null)
{
element.BringIntoView();
_bringIntoViewContainer = element;
}
}
}
}
#endregion
#endregion
//-------------------------------------------------------------------
//
// Public Properties
//
//--------------------------------------------------------------------
#region Public Properties
///
/// Specifies dimension of children stacking.
///
public Orientation Orientation
{
get { return (Orientation) GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
///
/// This property is always true because this panel has vertical or horizontal orientation
///
protected internal override bool HasLogicalOrientation
{
get { return true; }
}
///
/// Orientation of the panel if its layout is in one dimension.
/// Otherwise HasLogicalOrientation is false and LogicalOrientation should be ignored
///
protected internal override Orientation LogicalOrientation
{
get { return this.Orientation; }
}
///
/// DependencyProperty for property.
///
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(Orientation.Vertical,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnOrientationChanged)),
new ValidateValueCallback(ScrollBar.IsValidOrientation));
///
/// Attached property for use on the ItemsControl that is the host for the items being
/// presented by this panel. Use this property to turn virtualization on/off.
///
public static readonly DependencyProperty IsVirtualizingProperty =
DependencyProperty.RegisterAttached("IsVirtualizing", typeof(bool), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(true));
///
/// Retrieves the value for .
///
/// The object on which to query the value.
/// True if virtualizing, false otherwise.
public static bool GetIsVirtualizing(DependencyObject o)
{
if (o == null)
{
throw new ArgumentNullException("o");
}
return (bool)o.GetValue(IsVirtualizingProperty);
}
///
/// Sets the value for .
///
/// The element on which to set the value.
/// True if virtualizing, false otherwise.
public static void SetIsVirtualizing(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(IsVirtualizingProperty, value);
}
///
/// Attached property for use on the ItemsControl that is the host for the items being
/// presented by this panel. Use this property to modify the virtualization mode.
///
/// Note that this property can only be set before the panel has been initialized
///
public static readonly DependencyProperty VirtualizationModeProperty =
DependencyProperty.RegisterAttached("VirtualizationMode", typeof(VirtualizationMode), typeof(VirtualizingStackPanel),
new FrameworkPropertyMetadata(VirtualizationMode.Standard));
///
/// Retrieves the value for .
///
/// The object on which to query the value.
/// The current virtualization mode.
public static VirtualizationMode GetVirtualizationMode(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (VirtualizationMode)element.GetValue(VirtualizationModeProperty);
}
///
/// Sets the value for .
///
/// The element on which to set the value.
/// The desired virtualization mode.
public static void SetVirtualizationMode(DependencyObject element, VirtualizationMode value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(VirtualizationModeProperty, value);
}
//-----------------------------------------------------------
// IScrollInfo Properties
//-----------------------------------------------------------
#region IScrollInfo Properties
///
/// VirtualizingStackPanel reacts to this property by changing its child measurement algorithm.
/// If scrolling in a dimension, infinite space is allowed the child; otherwise, available size is preserved.
///
[DefaultValue(false)]
public bool CanHorizontallyScroll
{
get
{
if (_scrollData == null) { return false; }
return _scrollData._allowHorizontal;
}
set
{
EnsureScrollData();
if (_scrollData._allowHorizontal != value)
{
_scrollData._allowHorizontal = value;
InvalidateMeasure();
}
}
}
///
/// VirtualizingStackPanel reacts to this property by changing its child measurement algorithm.
/// If scrolling in a dimension, infinite space is allowed the child; otherwise, available size is preserved.
///
[DefaultValue(false)]
public bool CanVerticallyScroll
{
get
{
if (_scrollData == null) { return false; }
return _scrollData._allowVertical;
}
set
{
EnsureScrollData();
if (_scrollData._allowVertical != value)
{
_scrollData._allowVertical = value;
InvalidateMeasure();
}
}
}
///
/// ExtentWidth contains the horizontal size of the scrolled content element in 1/96"
///
public double ExtentWidth
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._extent.Width;
}
}
///
/// ExtentHeight contains the vertical size of the scrolled content element in 1/96"
///
public double ExtentHeight
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._extent.Height;
}
}
///
/// ViewportWidth contains the horizontal size of content's visible range in 1/96"
///
public double ViewportWidth
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._viewport.Width;
}
}
///
/// ViewportHeight contains the vertical size of content's visible range in 1/96"
///
public double ViewportHeight
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._viewport.Height;
}
}
///
/// HorizontalOffset is the horizontal offset of the scrolled content in 1/96".
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public double HorizontalOffset
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._computedOffset.X;
}
}
///
/// VerticalOffset is the vertical offset of the scrolled content in 1/96".
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public double VerticalOffset
{
get
{
if (_scrollData == null) { return 0.0; }
return _scrollData._computedOffset.Y;
}
}
///
/// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant
/// on this IScrollInfo's properties.
///
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ScrollViewer ScrollOwner
{
get
{
EnsureScrollData();
return _scrollData._scrollOwner;
}
set
{
EnsureScrollData();
if (value != _scrollData._scrollOwner)
{
ResetScrolling(this);
_scrollData._scrollOwner = value;
}
}
}
#endregion IScrollInfo Properties
#endregion Public Properties
//-------------------------------------------------------------------
//
// Public Events
//
//--------------------------------------------------------------------
#region Public Events
///
/// Called on the ItemsControl that owns this panel when an item is being re-virtualized.
///
public static readonly RoutedEvent CleanUpVirtualizedItemEvent = EventManager.RegisterRoutedEvent("CleanUpVirtualizedItemEvent", RoutingStrategy.Direct, typeof(CleanUpVirtualizedItemEventHandler), typeof(VirtualizingStackPanel));
///
/// Adds a handler for the CleanUpVirtualizedItem attached event
///
/// DependencyObject that listens to this event
/// Event Handler to be added
public static void AddCleanUpVirtualizedItemHandler(DependencyObject element, CleanUpVirtualizedItemEventHandler handler)
{
FrameworkElement.AddHandler(element, CleanUpVirtualizedItemEvent, handler);
}
///
/// Removes a handler for the CleanUpVirtualizedItem attached event
///
/// DependencyObject that listens to this event
/// Event Handler to be removed
public static void RemoveCleanUpVirtualizedItemHandler(DependencyObject element, CleanUpVirtualizedItemEventHandler handler)
{
FrameworkElement.RemoveHandler(element, CleanUpVirtualizedItemEvent, handler);
}
///
/// Called when an item is being re-virtualized.
///
protected virtual void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs e)
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (itemsControl != null)
{
itemsControl.RaiseEvent(e);
}
}
#endregion
//-------------------------------------------------------------------
//
// Protected Methods
//
//--------------------------------------------------------------------
#region Protected Methods
///
/// General VirtualizingStackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
/// Children in this dimension are encouraged to be as large as they like. In the other dimension,
/// VirtualizingStackPanel will assume the maximum size of its children.
///
///
/// When scrolling, VirtualizingStackPanel will not grow in layout size but effectively add the children on a z-plane which
/// will probably be clipped by some parent (typically a ScrollContentPresenter) to Stack's size.
///
/// Constraint
/// Desired size
protected override Size MeasureOverride(Size constraint)
{
#if Profiling
if (Panel.IsAboutToGenerateContent(this))
return MeasureOverrideProfileStub(constraint);
else
return RealMeasureOverride(constraint);
}
// this is a handy place to start/stop profiling
private Size MeasureOverrideProfileStub(Size constraint)
{
return RealMeasureOverride(constraint);
}
private Size RealMeasureOverride(Size constraint)
{
#endif
bool etwTracingEnabled = IsScrolling && EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal);
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.StartEvent, "VirtualizingStackPanel :MeasureOverride");
}
Debug.Assert(MeasureData == null || constraint == MeasureData.AvailableSize, "MeasureData needs to be passed down in [....] with size");
MeasureData measureData = MeasureData;
Size stackDesiredSize = new Size();
Size layoutSlotSize = constraint;
bool fHorizontal = (Orientation == Orientation.Horizontal);
int firstViewport; // First child index in the viewport.
double firstItemOffset; // Offset of the top of the first child relative to the top of the viewport.
double virtualizedItemsSize = 0d; // Amount that virtualized children contribute to the desired size in the stacking direction
int lastViewport = -1; // Last child index in the viewport. -1 indicates we have not yet iterated through the last child.
double logicalVisibleSpace, childLogicalSize;
Rect originalViewport = Rect.Empty; // Only used if this is the scrolling panel. Saves off the given viewport for scroll computations.
// Collect information from the ItemsControl, if there is one.
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = (itemsControl != null) ? itemsControl.Items.Count : 0;
SetVirtualizationState(itemsControl, /* hasMeasureData = */ measureData != null && measureData.HasViewport);
IList children = RealizedChildren; // yes, this is weird, but this property ensures the Generator is properly initialized.
IItemContainerGenerator generator = Generator;
// Adjust the viewport
if (IsPixelBased)
{
if (IsScrolling)
{
// We're the top level scrolling panel. Set the viewport and extend it to add a focus trail
originalViewport = new Rect(_scrollData._offset.X, _scrollData._offset.Y, constraint.Width, constraint.Height);
measureData = new MeasureData(constraint, originalViewport);
// The way we have a focus trail when pixel-based is to artificially extend the viewport. All calculations are done
// with this 'artificial' viewport with the exception of the scroll offset, extent, etc.
measureData = AddFocusTrail(measureData, fHorizontal);
Debug.Assert(!object.ReferenceEquals(originalViewport, measureData.Viewport), "original viewport should not have a focus trail");
}
else
{
measureData = AdjustViewportOffset(measureData, itemsControl, fHorizontal);
Debug.Assert(!object.ReferenceEquals(MeasureData, measureData), "The value set in the MeasureData property should not be modified");
}
}
//
// Initialize child sizing and iterator data
// Allow children as much size as they want along the stack.
//
if (fHorizontal)
{
layoutSlotSize.Width = Double.PositiveInfinity;
if (IsScrolling && CanVerticallyScroll) { layoutSlotSize.Height = Double.PositiveInfinity; }
logicalVisibleSpace = constraint.Width;
}
else
{
layoutSlotSize.Height = Double.PositiveInfinity;
if (IsScrolling && CanHorizontallyScroll) { layoutSlotSize.Width = Double.PositiveInfinity; }
logicalVisibleSpace = constraint.Height;
}
// Compute index of first item in the viewport
firstViewport = ComputeIndexOfFirstVisibleItem(measureData, itemsControl, fHorizontal, out firstItemOffset);
if (IsPixelBased)
{
// Acount for the size of items that won't be generated
Debug.Assert(stackDesiredSize.Width == 0 && stackDesiredSize.Height == 0, "stack desired size must be 0 for virtualizedItemsSize to work");
stackDesiredSize = ExtendDesiredSize(itemsControl, stackDesiredSize, firstViewport, /*before = */ true, fHorizontal);
virtualizedItemsSize = fHorizontal ? stackDesiredSize.Width : stackDesiredSize.Height;
}
debug_AssertRealizedChildrenEqualVisualChildren();
// If recycling clean up before generating children.
if (IsVirtualizing && InRecyclingMode)
{
CleanupContainers(firstViewport, itemsControl);
debug_VerifyRealizedChildren();
}
//
// Figure out the position of the first visible item
//
GeneratorPosition startPos = IndexToGeneratorPositionForStart(IsVirtualizing ? firstViewport : 0, out _firstVisibleChildIndex);
int childIndex = _firstVisibleChildIndex;
//
// Main loop: generate and measure all children (or all visible children if virtualizing).
//
bool ranOutOfItems = true;
bool visualOrderChanged = false;
_visibleCount = 0;
if (itemCount > 0)
{
_afterTrail = 0;
using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
for (int i = IsVirtualizing ? firstViewport : 0, count = itemCount; i < count; ++i)
{
// Get next child.
bool newlyRealized;
UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
if (child == null)
{
Debug.Assert(!newlyRealized, "The generator realized a null value.");
// We reached the end of the items (because of a group)
break;
}
visualOrderChanged |= AddContainerFromGenerator(childIndex, child, newlyRealized);
childIndex++;
_visibleCount++;
if (IsPixelBased)
{
// Pass along MeasureData so it continues down the tree.
child.MeasureData = CreateChildMeasureData(measureData, layoutSlotSize, stackDesiredSize, fHorizontal);
}
Size childDesiredSize = child.DesiredSize;
child.Measure(layoutSlotSize);
if (childDesiredSize != child.DesiredSize)
{
childDesiredSize = child.DesiredSize;
// Reset the _maxDesiredSize cache if child DesiredSize changes
if (_scrollData != null)
_scrollData._maxDesiredSize = new Size();
}
// Accumulate child size.
if (fHorizontal)
{
stackDesiredSize.Width += childDesiredSize.Width;
stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height);
childLogicalSize = childDesiredSize.Width;
}
else
{
stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
stackDesiredSize.Height += childDesiredSize.Height;
childLogicalSize = childDesiredSize.Height;
}
// Adjust remaining viewport space if we are scrolling and within the viewport region.
// While scrolling (not virtualizing), we always measure children before and after the viewport.
if (IsScrolling && lastViewport == -1 && i >= firstViewport)
{
logicalVisibleSpace -= childLogicalSize;
if (DoubleUtil.LessThanOrClose(logicalVisibleSpace, 0.0))
{
lastViewport = i;
}
}
// When under a viewport, virtualizing and at or beyond the first element, stop creating elements when out of space.
if (IsVirtualizing && (i >= firstViewport))
{
double viewportSize;
double totalGenerated;
//
// Decide if the end of the item is outside the viewport.
//
// StackDesiredSize, with some adjustment, is a measure of exactly how much viewport space we have used.
//
// StackDesiredSize is the sum of all generated children (starting with the first visible item). The first
// visible item doesn't always start at the top of the viewport, so we have to adjust by the firstItemoffset.
//
// When pixel-based we add the sum of all virtualized children to the stackDesiredSize; this has to be removed as well.
//
Debug.Assert(IsPixelBased || virtualizedItemsSize == 0d);
if (fHorizontal)
{
viewportSize = IsPixelBased ? measureData.Viewport.Width : constraint.Width;
totalGenerated = stackDesiredSize.Width - virtualizedItemsSize + firstItemOffset;
}
else
{
viewportSize = IsPixelBased ? measureData.Viewport.Height : constraint.Height;
totalGenerated = stackDesiredSize.Height - virtualizedItemsSize + firstItemOffset;
}
if (totalGenerated > viewportSize)
{
// The end of this child is outside the viewport. Check if we want to generate some more.
if (IsPixelBased)
{
// For pixel-based virtualization (specifically TreeView virtualization) we deal with
// the after trail later, since it has to function hierarchically.
break;
}
else
{
// We want to keep a focusable item after the end so that keyboard navigation
// can work, but we want to limit that to FocusTrail number of items
// in case all the items are not focusable.
if (_afterTrail > 0 && ( _afterTrail >= FocusTrail || Keyboard.IsFocusable(child)))
{
// Either we passed the limit or the child was focusable
ranOutOfItems = false;
break;
}
_afterTrail++;
// Loop around and generate another item
}
}
}
}
}
}
#if DEBUG
if (IsVirtualizing && InRecyclingMode)
{
debug_VerifyRealizedChildren();
}
#endif
_visibleStart = firstViewport;
if (IsPixelBased)
{
// Acount for the size of items that won't be generated
stackDesiredSize = ExtendDesiredSize(itemsControl, stackDesiredSize, firstViewport + _visibleCount, /*before = */ false, fHorizontal);
}
//
// Adjust the scroll offset, extent, etc.
//
if (IsScrolling)
{
if (IsPixelBased)
{
Vector offset = new Vector(originalViewport.Location.X, originalViewport.Location.Y);
SetAndVerifyScrollingData(originalViewport.Size, stackDesiredSize, offset);
}
else
{
// Compute the extent before we fill remaining space and modify the stack desired size
Size extent = ComputeLogicalExtent(stackDesiredSize, itemCount, fHorizontal);
if (ranOutOfItems)
{
// If we or children have resized, it's possible that we can now display more content.
// This is true if we started at a nonzero offeset and still have space remaining.
// In this case, we loop back through previous children until we run out of space.
FillRemainingSpace(ref firstViewport, ref logicalVisibleSpace, ref stackDesiredSize, layoutSlotSize, fHorizontal);
}
// Create the Before focus trail
// NOTE: the call here (under IsScrolling) implicitly assumes that only a scrolling panel can virtualize and thus requires
// a focus trail. That's not true for hierarchical (pixel-based) virtualization, but it handles the focus trail differently anyway.
EnsureTopCapGenerated(layoutSlotSize);
// Compute Scrolling data such as extent, viewport, and offset.
stackDesiredSize = UpdateLogicalScrollData(stackDesiredSize, constraint, logicalVisibleSpace,
extent, firstViewport, lastViewport, itemCount, fHorizontal);
}
}
//
// Cleanup items no longer in the viewport
//
if (IsVirtualizing && !InRecyclingMode)
{
if (IsPixelBased)
{
// Immediate cleanup
CleanupContainers(firstViewport, itemsControl);
}
else
{
// Less aggressive backwards-compat background cleanup operation
EnsureCleanupOperation(false /* delay */);
}
}
if (IsVirtualizing && InRecyclingMode)
{
DisconnectRecycledContainers();
if (visualOrderChanged)
{
// We moved some containers in the visual tree without firing changed events. ZOrder is now invalid.
InvalidateZState();
}
}
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.EndEvent, "VirtualizingStackPanel :MeasureOverride");
}
debug_AssertRealizedChildrenEqualVisualChildren();
return stackDesiredSize;
}
///
/// Content arrangement.
///
/// Arrange size
protected override Size ArrangeOverride(Size arrangeSize)
{
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(arrangeSize);
IList children;
double previousChildSize = 0.0;
ItemsControl itemsControl = null;
bool childrenAreContainers = true;
bool etwTracingEnabled = IsScrolling && EventTrace.IsEnabled(EventTrace.Flags.performance, EventTrace.Level.normal);
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.StartEvent, "VirtualizingStackPanel :ArrangeOverride");
}
//
// Compute scroll offset and seed it into rcChild.
//
if (IsScrolling)
{
if (fHorizontal)
{
double offsetX = _scrollData._computedOffset.X;
rcChild.X = IsPixelBased ? -offsetX : ComputePhysicalFromLogicalOffset(IsVirtualizing ? _firstVisibleChildIndex : offsetX, true);
rcChild.Y = -1.0 * _scrollData._computedOffset.Y;
}
else
{
double offsetY = _scrollData._computedOffset.Y;
rcChild.X = -1.0 * _scrollData._computedOffset.X;
rcChild.Y = IsPixelBased ? -offsetY : ComputePhysicalFromLogicalOffset(IsVirtualizing ? _firstVisibleChildIndex : offsetY, false);
}
}
//
// Arrange and Position Children.
//
// If we're virtualizing and pixel-based we loop through the entire items collection (the policy is to arrange items exactly where they
// should appear regardless of the virtualization state of siblings). This is required to properly virtualize hiearchically.
// Otherwise we loop through the children collection (when virtualizing in items mode VSP arranges children in a simple stack order).
//
if (IsPixelBased && IsVirtualizing)
{
// This is a pixel-based internal panel. It must behave externally exactly the way a non-virtualizing panel does in Arrange.
// Specifically, it arranges its children in the 'proper' place, regardless of whether or not their siblings are virtualized.
itemsControl = ItemsControl.GetItemsOwner(this);
children = itemsControl.Items;
childrenAreContainers = false;
}
else
{
debug_AssertRealizedChildrenEqualVisualChildren(); // RealizedChildren only differs from InternalChildren inside of Measure when container recycling is on.
children = RealizedChildren;
}
for (int i = 0; i < children.Count; ++i)
{
UIElement container = null;
Size childSize;
if (childrenAreContainers)
{
// we are looping through the actual containers; the visual children of this panel.
container = (UIElement)children[i];
childSize = container.DesiredSize;
}
else
{
// We are looping through items and may or may not have a container for each given item.
childSize = ContainerSizeForItem(itemsControl, children[i], i, out container);
}
if (fHorizontal)
{
rcChild.X += previousChildSize;
previousChildSize = childSize.Width;
rcChild.Width = previousChildSize;
rcChild.Height = Math.Max(arrangeSize.Height, childSize.Height);
}
else
{
rcChild.Y += previousChildSize;
previousChildSize = childSize.Height;
rcChild.Height = previousChildSize;
rcChild.Width = Math.Max(arrangeSize.Width, childSize.Width);
}
if (container != null)
{
container.Arrange(rcChild);
}
}
if (etwTracingEnabled)
{
EventTrace.EventProvider.TraceEvent(EventTrace.GuidFromId(EventTraceGuidId.GENERICSTRINGGUID), MS.Utility.EventType.EndEvent, "VirtualizingStackPanel :ArrangeOverride");
}
return arrangeSize;
}
///
/// Called when the Items collection associated with the containing ItemsControl changes.
///
/// sender
/// Event arguments
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
base.OnItemsChanged(sender, args);
bool resetMaximumDesiredSize = false;
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
OnItemsRemove(args);
resetMaximumDesiredSize = true;
break;
case NotifyCollectionChangedAction.Replace:
OnItemsReplace(args);
resetMaximumDesiredSize = true;
break;
case NotifyCollectionChangedAction.Move:
OnItemsMove(args);
break;
case NotifyCollectionChangedAction.Reset:
resetMaximumDesiredSize = true;
break;
}
if (resetMaximumDesiredSize && IsScrolling)
{
// The items changed such that the maximum size may no longer be valid.
// The next layout pass will update this value.
_scrollData._maxDesiredSize = new Size();
}
}
///
/// Called when the UI collection of children is cleared by the base Panel class.
///
protected override void OnClearChildren()
{
base.OnClearChildren();
_realizedChildren = null;
_visibleStart = _firstVisibleChildIndex = _visibleCount = 0;
}
// Override of OnGotKeyboardFocus. Called when focus moves to any child or subchild of this VSP
// Used by TreeView virtualization to keep track of the focused item.
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnGotKeyboardFocus(e);
FocusChanged(e);
}
// Override of OnLostKeyboardFocus. Called when focus moves away from this VSP.
// Used by TreeView virtualization to keep track of the focused item.
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
FocusChanged(e);
}
#endregion Protected Methods
#region Internal Methods
// Tells the Generator to clear out all containers for this ItemsControl. This is called by the ItemValueStorage
// service when the ItemsControl this panel is a host for is about to be thrown away. This allows the VSP to save
// off any properties it is interested in and results in a call to ClearContainerForItem on the ItemsControl, allowing
// the Item Container Storage to do so as well.
// Note: A possible perf improvement may be to make 'fast' RemoveAll on the Generator that simply calls ClearContainerForItem
// for us without walking through its data structures to actually clean out items.
internal void ClearAllContainers(ItemsControl itemsControl)
{
Debug.Assert(IsVirtualizing,
"We should only clear containers for ItemsControls that are virtualizing");
Debug.Assert(itemsControl == ItemsControl.GetItemsOwner(this),
"We can only clear containers that this panel is a host for");
IItemContainerGenerator generator = Generator;
if (IsPixelBased)
{
IList children = RealizedChildren;
UIElement child;
for (int i = 0; i < children.Count; i++)
{
child = (UIElement)children[i];
itemsControl.StoreItemValue(((ItemContainerGenerator)generator).ItemFromContainer(child), child.DesiredSize, _desiredSizeStorageIndex);
}
}
if (generator != null)
{
generator.RemoveAll();
}
}
#endregion
//------------------------------------------------------
//
// Private Methods
//
//-----------------------------------------------------
#region Private Methods
//
// MeasureOverride Helpers
//
#region MeasureOverride Helpers
///
/// Extends the viewport of the given MeasureData to give a focus trail. Returns by how much it extended the viewport.
///
///
///
private MeasureData AddFocusTrail(MeasureData measureData, bool isHorizontal)
{
//
// Create the before / after focus trail for interior panels that use MeasureData's viewport to virtualize.
// We expand the viewport so that roughly two extra items are generated at the top and the bottom.
//
// For the before / after focus trail good values are
// padding = header height * 4;
//
// To make page up / down work without rewriting TreeView's algorithm we actually extend the viewport one extra page
// top and bottm.
//
Debug.Assert(IsScrolling, "The scrolling panel is the only one that should extend the viewport");
Invariant.Assert(IsPixelBased, "If we're sending down a viewport to the children we should be doing pixel-based computations");
double page = isHorizontal ? ViewportWidth : ViewportHeight;
Rect viewport = measureData.Viewport;
if (isHorizontal)
{
viewport.Width += page * 2;
viewport.X -= page;
}
else
{
viewport.Height += page * 2;
viewport.Y -= page;
}
measureData.Viewport = viewport;
return measureData;
}
#region Scroll Computation Helpers
///
/// Returns the extent in logical units in the stacking direction.
///
///
///
///
///
private Size ComputeLogicalExtent(Size stackDesiredSize, int itemCount, bool isHorizontal)
{
bool accumulateExtent = false;
Size extent = new Size();
if (ScrollOwner != null)
{
accumulateExtent = ScrollOwner.InChildInvalidateMeasure;
ScrollOwner.InChildInvalidateMeasure = false;
}
if (isHorizontal)
{
extent.Width = itemCount;
extent.Height = accumulateExtent ? Math.Max(stackDesiredSize.Height, _scrollData._extent.Height) : stackDesiredSize.Height;
}
else
{
extent.Width = accumulateExtent ? Math.Max(stackDesiredSize.Width, _scrollData._extent.Width) : stackDesiredSize.Width;
extent.Height = itemCount;
}
return extent;
}
///
/// Called when we ran out of children before filling up the viewport.
///
private void FillRemainingSpace(ref int firstViewport, ref double logicalVisibleSpace, ref Size stackDesiredSize, Size layoutSlotSize, bool isHorizontal)
{
Debug.Assert(IsScrolling, "Only the scrolling panel can fill remaining space");
Debug.Assert(!IsPixelBased, "This is a logical operation");
double projectedLogicalVisibleSpace;
Size childDesiredSize;
IList children = RealizedChildren;
int childIndex = IsVirtualizing ? _firstVisibleChildIndex : firstViewport;
while (childIndex > 0)
{
if (!PreviousChildIsGenerated(childIndex))
{
GeneratePreviousChild(childIndex, layoutSlotSize);
childIndex++; // We just inserted a child, so increment the index
}
else if (childIndex <= _firstVisibleChildIndex)
{
((UIElement)children[childIndex - 1]).Measure(layoutSlotSize);
}
projectedLogicalVisibleSpace = logicalVisibleSpace;
childDesiredSize = ((UIElement)children[childIndex - 1]).DesiredSize;
if (isHorizontal)
{
projectedLogicalVisibleSpace -= childDesiredSize.Width;
}
else
{
projectedLogicalVisibleSpace -= childDesiredSize.Height;
}
// If we have run out of room, break.
if (DoubleUtil.LessThan(projectedLogicalVisibleSpace, 0.0)) { break; }
// Account for the child in the panel's desired size
if (isHorizontal)
{
stackDesiredSize.Width += childDesiredSize.Width;
stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height);
}
else
{
stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
stackDesiredSize.Height += childDesiredSize.Height;
}
// Adjust viewport
childIndex--;
logicalVisibleSpace = projectedLogicalVisibleSpace;
_visibleCount++;
}
if ((childIndex < _firstVisibleChildIndex) || !IsVirtualizing)
{
_firstVisibleChildIndex = childIndex;
}
_visibleStart = firstViewport = (IsItemsHost && children.Count != 0) ? GetGeneratedIndex(_firstVisibleChildIndex) : 0;
}
///
/// Updates ScrollData's offset, extent, and viewport in logical units.
///
///
///
///
///
///
///
///
///
///
private Size UpdateLogicalScrollData(Size stackDesiredSize, Size constraint, double logicalVisibleSpace, Size extent,
int firstViewport, int lastViewport, int itemCount, bool fHorizontal)
{
Debug.Assert(IsScrolling && !IsPixelBased, "this computes logical scroll data");
Size viewport = constraint;
Vector offset = _scrollData._offset;
// If we have not yet set the last child in the viewport, set it to the last child.
if (lastViewport == -1) { lastViewport = itemCount - 1; }
int logicalExtent = itemCount;
int logicalViewport = lastViewport - firstViewport;
//
// Compute the logical viewport size.
//
// We are conservative when estimating a viewport, not including the last element in case it is only partially visible.
// We want to count it if it is fully visible (>= 0 space remaining) or the only element in the viewport.
if (logicalViewport == 0 || DoubleUtil.GreaterThanOrClose(logicalVisibleSpace, 0.0)) { logicalViewport++; }
if (fHorizontal)
{
viewport.Width = logicalViewport;
offset.X = firstViewport;
offset.Y = Math.Max(0, Math.Min(offset.Y, extent.Height - viewport.Height));
// In case last item is visible because we scroll all the way to the right and scrolling is on
// we want desired size not to be smaller than constraint to avoid another relayout
if (logicalExtent > logicalViewport && !Double.IsPositiveInfinity(constraint.Width))
{
stackDesiredSize.Width = constraint.Width;
}
}
else
{
viewport.Height = logicalViewport;
offset.Y = firstViewport;
offset.X = Math.Max(0, Math.Min(offset.X, extent.Width - viewport.Width));
// In case last item is visible because we scroll all the way to the bottom and scrolling is on
// we want desired size not to be smaller than constraint to avoid another relayout
if (logicalExtent > logicalViewport && !Double.IsPositiveInfinity(constraint.Height))
{
stackDesiredSize.Height = constraint.Height;
}
}
// Since we can offset and clip our content, we never need to be larger than the parent suggestion.
// If we returned the full size of the content, we would always be so big we didn't need to scroll. :)
stackDesiredSize.Width = Math.Min(stackDesiredSize.Width, constraint.Width);
stackDesiredSize.Height = Math.Min(stackDesiredSize.Height, constraint.Height);
// When scrolling, the maximum horizontal or vertical size of items can cause the desired size of the
// panel to change, which can cause the owning ScrollViewer re-layout as well when it is not necessary.
// We will thus remember the maximum desired size and always return that. The actual arrangement and
// clipping still be calculated from actual scroll data values.
// The maximum desired size is reset when the items change.
_scrollData._maxDesiredSize.Width = Math.Max(stackDesiredSize.Width, _scrollData._maxDesiredSize.Width);
_scrollData._maxDesiredSize.Height = Math.Max(stackDesiredSize.Height, _scrollData._maxDesiredSize.Height);
stackDesiredSize = _scrollData._maxDesiredSize;
// Verify Scroll Info, invalidate ScrollOwner if necessary.
SetAndVerifyScrollingData(viewport, extent, offset);
return stackDesiredSize;
}
#endregion
///
/// DesiredSize is normally computed by summing up the size of all items we've generated. Pixel-based virtualization uses a 'full' desired size.
/// This extends the given desired size beyond the visible items. It will extend it by the items before or after the set of generated items.
/// The given pivotIndex is the index of either the first or last item generated.
///
///
///
///
///
///
///
private Size ExtendDesiredSize(ItemsControl itemsControl, Size stackDesiredSize, int pivotIndex, bool before, bool isHorizontal)
{
Debug.Assert(IsPixelBased, "MeasureOverride should have already computed desiredSize if non-virtualizing or items-based");
//
// If we're virtualizing the sum of all generated containers is not the true desired size since not all containers were generated.
// In the old items-based mode it didn't matter because only the scrolling panel could virtualize and scrollviewer doesn't *really*
// care about desired size.
//
// In pixel-based mode we need to compute the same desired size as if we weren't virtualizing.
//
// Note: there are faster ways to do this than loop through items, but the cost isn't significant and the other possible implementations are nasty.
//
Size containerSize;
ItemCollection items = itemsControl.Items;
for (int i = (before ? 0 : pivotIndex); i < (before ? pivotIndex : items.Count); i++)
{
containerSize = ContainerSizeForItem(itemsControl, items[i], i);
if (isHorizontal)
{
stackDesiredSize.Width += containerSize.Width;
}
else
{
stackDesiredSize.Height += containerSize.Height;
}
}
return stackDesiredSize;
}
//
// Returns the index of the first item visible (even partially) in the viewport.
//
private int ComputeIndexOfFirstVisibleItem(MeasureData measureData, ItemsControl itemsControl, bool isHorizontal, out double firstItemOffset)
{
firstItemOffset = 0d; // offset of the top of the first visible child from the top of the viewport. The child always
// starts before the top of the viewport so this is always negative.
if (itemsControl != null)
{
ItemCollection items = itemsControl.Items;
int itemsCount = items.Count;
if (!IsPixelBased)
{
//
// Classic case that shipped with V1
//
// If the panel is implementing IScrollInfo then _scrollData keeps track of the
// current offset, extent, etc in logical units
//
if (IsScrolling)
{
return CoerceIndexToInteger(isHorizontal ? _scrollData._offset.X : _scrollData._offset.Y, itemsCount);
}
}
else
{
Size containerSize;
double totalSpan = 0.0; // total height or width in the stacking direction
double containerSpan = 0.0;
double viewportOffset = isHorizontal ? measureData.Viewport.X : measureData.Viewport.Y;
for (int i = 0; i < itemsCount; i++)
{
containerSize = ContainerSizeForItem(itemsControl, items[i], i);
containerSpan = isHorizontal ? containerSize.Width : containerSize.Height;
totalSpan += containerSpan;
if (totalSpan > viewportOffset)
{
// This is the first item that starts before the viewportOffset but ends after it; i is thus the index
// to the first item in the viewport.
firstItemOffset = totalSpan - containerSpan - viewportOffset;
return i;
}
}
}
}
return 0;
}
private Size ContainerSizeForItem(ItemsControl itemsControl, object item, int index)
{
UIElement temp;
return ContainerSizeForItem(itemsControl, item, index, out temp);
}
///
/// Returns the size of the container for a given item. The size can come from the container, a lookup, or a guess depending
/// on the virtualization state of the item.
///
///
///
///
/// returns the container for the item; null if the container wasn't found
///
private Size ContainerSizeForItem(ItemsControl itemsControl, object item, int index, out UIElement container)
{
Size containerSize;
container = index >= 0 ? ((ItemContainerGenerator)Generator).ContainerFromIndex(index) as UIElement : null;
if (container != null)
{
containerSize = container.DesiredSize;
}
else
{
// It's virtualized; grab the height off the item if available.
object value = itemsControl.ReadItemValue(item, _desiredSizeStorageIndex);
if (value != null)
{
containerSize = (Size)value;
}
else
{
//
// No stored container height; simply guess.
//
containerSize = new Size();
if (Orientation == Orientation.Horizontal)
{
containerSize.Width = ContainerStackingSizeEstimate(itemsControl, /*isHorizontal = */ true);
containerSize.Height = DesiredSize.Height;
}
else
{
containerSize.Height = ContainerStackingSizeEstimate(itemsControl, /*isHorizontal = */ false);
containerSize.Width = DesiredSize.Width;
}
}
}
Debug.Assert(!containerSize.IsEmpty, "We can't estimate an empty size");
return containerSize;
}
private double ContainerStackingSizeEstimate(ItemsControl itemsControl, bool isHorizontal)
{
return ContainerStackingSizeEstimate(itemsControl as IProvideStackingSize, isHorizontal);
}
///
/// Estimates a container size in the stacking direction for the given ItemsControl
///
///
///
///
private double ContainerStackingSizeEstimate(IProvideStackingSize estimate, bool isHorizontal)
{
double stackingSize = 0d;
if (estimate != null)
{
stackingSize = estimate.EstimatedContainerSize(isHorizontal);
}
if (stackingSize <= 0d || DoubleUtil.IsNaN(stackingSize))
{
stackingSize = ScrollViewer._scrollLineDelta;
}
Debug.Assert(stackingSize > 0, "We should have returned a reasonable estimate for the stacking size");
return stackingSize;
}
private MeasureData CreateChildMeasureData(MeasureData measureData, Size layoutSlotSize, Size stackDesiredSize, bool isHorizontal)
{
Invariant.Assert(IsPixelBased && measureData != null, "We can only use MeasureData when pixel-based");
Rect viewport = measureData.Viewport;
//
// Adjust viewport offset for the child
//
if (isHorizontal)
{
viewport.X -= stackDesiredSize.Width;
}
else
{
viewport.Y -= stackDesiredSize.Height;
}
return new MeasureData(layoutSlotSize, viewport);
}
///
/// Inserts a new container in the visual tree
///
///
///
private void InsertNewContainer(int childIndex, UIElement container)
{
InsertContainer(childIndex, container, false);
}
///
/// Inserts a recycled container in the visual tree
///
///
///
///
private bool InsertRecycledContainer(int childIndex, UIElement container)
{
return InsertContainer(childIndex, container, true);
}
///
/// Inserts a container into the Children collection. The container is either new or recycled.
///
///
///
///
private bool InsertContainer(int childIndex, UIElement container, bool isRecycled)
{
Debug.Assert(container != null, "Null container was generated");
bool visualOrderChanged = false;
UIElementCollection children = InternalChildren;
//
// Find the index in the Children collection where we hope to insert the container.
// This is done by looking up the index of the container BEFORE the one we hope to insert.
//
// We have to do it this way because there could be recycled containers between the container we're looking for and the one before it.
// By finding the index before the place we want to insert and adding one, we ensure that we'll insert the new container in the
// proper location.
//
// In recycling mode childIndex is the index in the _realizedChildren list, not the index in the
// Children collection. We have to convert the index; we'll call the index in the Children collection
// the visualTreeIndex.
//
int visualTreeIndex = 0;
if (childIndex > 0)
{
visualTreeIndex = ChildIndexFromRealizedIndex(childIndex - 1);
visualTreeIndex++;
}
if (isRecycled && visualTreeIndex < children.Count && children[visualTreeIndex] == container)
{
// Don't insert if a recycled container is in the proper place already
}
else
{
if (visualTreeIndex < children.Count)
{
int insertIndex = visualTreeIndex;
if (isRecycled && container.InternalVisualParent != null)
{
// If the container is recycled we have to remove it from its place in the visual tree and
// insert it in the proper location. For perf we'll use an internal Move API that moves
// the first parameter to right before the second one.
Debug.Assert(children[visualTreeIndex] != null, "MoveVisualChild interprets a null destination as 'move to end'");
children.MoveVisualChild(container, children[visualTreeIndex]);
visualOrderChanged = true;
}
else
{
VirtualizingPanel.InsertInternalChild(children, insertIndex, container);
}
}
else
{
if (isRecycled && container.InternalVisualParent != null)
{
// Recycled container is still in the tree; move it to the end
children.MoveVisualChild(container, null);
visualOrderChanged = true;
}
else
{
VirtualizingPanel.AddInternalChild(children, container);
}
}
}
//
// Keep realizedChildren in [....] w/ the visual tree.
//
if (IsVirtualizing && InRecyclingMode)
{
_realizedChildren.Insert(childIndex, container);
}
Generator.PrepareItemContainer(container);
return visualOrderChanged;
}
private void EnsureCleanupOperation(bool delay)
{
if (delay)
{
bool noPendingOperations = true;
if (_cleanupOperation != null)
{
noPendingOperations = _cleanupOperation.Abort();
if (noPendingOperations)
{
_cleanupOperation = null;
}
}
if (noPendingOperations && (_cleanupDelay == null))
{
_cleanupDelay = new DispatcherTimer();
_cleanupDelay.Tick += new EventHandler(OnDelayCleanup);
_cleanupDelay.Interval = TimeSpan.FromMilliseconds(500.0);
_cleanupDelay.Start();
}
}
else
{
if ((_cleanupOperation == null) && (_cleanupDelay == null))
{
_cleanupOperation = Dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(OnCleanUp), null);
}
}
}
private bool PreviousChildIsGenerated(int childIndex)
{
GeneratorPosition position = new GeneratorPosition(childIndex, 0);
position = Generator.GeneratorPositionFromIndex(Generator.IndexFromGeneratorPosition(position) - 1);
return (position.Offset == 0 && position.Index >= 0);
}
///
/// Takes a container returned from Generator.GenerateNext() and places it in the visual tree if necessary.
/// Takes into account whether the container is new, recycled, or already realized.
///
///
///
///
private bool AddContainerFromGenerator(int childIndex, UIElement child, bool newlyRealized)
{
bool visualOrderChanged = false;
if (!newlyRealized)
{
//
// Container is either realized or recycled. If it's realized do nothing; it already exists in the visual
// tree in the proper place.
//
if (InRecyclingMode)
{
// Note there's no check for IsVirtualizing here. If the user has just flipped off virtualization it's possible that
// the Generator will still return some recycled containers until its list runs out.
IList children = RealizedChildren;
if (childIndex >= children.Count || !(children[childIndex] == child))
{
Debug.Assert(!children.Contains(child), "we incorrectly identified a recycled container");
//
// We have a recycled container (if it was a realized container it would have been returned in the
// proper location). Note also that recycled containers are NOT in the _realizedChildren list.
//
visualOrderChanged = InsertRecycledContainer(childIndex, child);
}
else
{
// previously realized child.
}
}
else
{
// Not recycling; realized container
Debug.Assert(child == InternalChildren[childIndex], "Wrong child was generated");
}
}
else
{
InsertNewContainer(childIndex, child);
}
return visualOrderChanged;
}
private UIElement GeneratePreviousChild(int childIndex, Size layoutSlotSize)
{
int newIndex = Generator.IndexFromGeneratorPosition(new GeneratorPosition(childIndex, 0)) - 1;
if (newIndex >= 0)
{
UIElement child;
bool visualOrderChanged = false;
IItemContainerGenerator generator = Generator;
int newGeneratedIndex;
GeneratorPosition newStartPos = IndexToGeneratorPositionForStart(newIndex, out newGeneratedIndex);
using (generator.StartAt(newStartPos, GeneratorDirection.Forward, true))
{
bool newlyRealized;
child = generator.GenerateNext(out newlyRealized) as UIElement;
Debug.Assert(child != null, "Null child was generated");
AddContainerFromGenerator(childIndex, child, newlyRealized);
if (childIndex <= _firstVisibleChildIndex)
{
_firstVisibleChildIndex++;
}
child.Measure(layoutSlotSize);
}
if (visualOrderChanged)
{
Debug.Assert(IsVirtualizing && InRecyclingMode, "We should only modify the visual order when in recycling mode");
InvalidateZState();
}
return child;
}
return null;
}
private void OnItemsRemove(ItemsChangedEventArgs args)
{
RemoveChildRange(args.Position, args.ItemCount, args.ItemUICount);
}
private void OnItemsReplace(ItemsChangedEventArgs args)
{
RemoveChildRange(args.Position, args.ItemCount, args.ItemUICount);
}
private void OnItemsMove(ItemsChangedEventArgs args)
{
RemoveChildRange(args.OldPosition, args.ItemCount, args.ItemUICount);
}
private void RemoveChildRange(GeneratorPosition position, int itemCount, int itemUICount)
{
if (IsItemsHost)
{
UIElementCollection children = InternalChildren;
int pos = position.Index;
if (position.Offset > 0)
{
// An item is being removed after the one at the index
pos++;
}
if (pos < children.Count)
{
int uiCount = itemUICount;
Debug.Assert((itemCount == itemUICount) || (itemUICount == 0), "Both ItemUICount and ItemCount should be equal or ItemUICount should be 0.");
if (uiCount > 0)
{
VirtualizingPanel.RemoveInternalChildRange(children, pos, uiCount);
if (IsVirtualizing && InRecyclingMode)
{
_realizedChildren.RemoveRange(pos, uiCount);
}
}
}
}
}
private void AdjustCacheWindow(int firstViewport, int itemCount)
{
//
// Adjust the container cache window such that the viewport is always contained inside.
//
// firstViewport is the index of the first container in the viewport, not counting the before trail.
// _visibleCount is the total number of items we generated. It already contains the _afterTrail.
// First and last containers that we must keep in view; index is into the data item collection
int firstContainer = firstViewport > 0 ? firstViewport - _beforeTrail : firstViewport;
int lastContainer = firstViewport + _visibleCount - 1; // beforeTrail is not included in _visibleCount
// clamp last container
if (lastContainer >= itemCount)
{
lastContainer = itemCount - 1;
}
int cacheEnd = CacheEnd;
if (firstContainer < _cacheStart)
{
// shift the cache start up
_cacheStart = firstContainer;
}
else if (lastContainer > cacheEnd)
{
// shift the cache start down
_cacheStart += (lastContainer - cacheEnd);
}
// In some cases cacheEnd can be past the end of the list of items. This is perfectly fine.
Debug.Assert(_cacheStart <= firstContainer && (CacheEnd >= firstContainer + _visibleCount - 1 || CacheEnd >= itemCount - 1), "The container cache window is out of place");
}
private bool IsOutsideCacheWindow(int itemIndex)
{
return (itemIndex < _cacheStart || itemIndex > CacheEnd);
}
///
/// Immediately cleans up any containers that have gone offscreen. Called by MeasureOverride.
/// When recycling this runs before generating and measuring children; otherwise it runs after.
///
private void CleanupContainers(int firstViewport, ItemsControl itemsControl)
{
Debug.Assert(IsVirtualizing, "Can't clean up containers if not virtualizing");
Debug.Assert(InRecyclingMode || IsPixelBased,
"For backwards compat the standard virtualizing mode has its own cleanup algorithm");
Debug.Assert(itemsControl != null, "We can't cleanup if we aren't the itemshost");
//
// It removes items outside of the container cache window (a logical 'window' at
// least as large as the viewport).
//
// firstViewport is the index of first data item that will be in the viewport
// at the end of Measure. This is effectively the scroll offset.
//
// _visibleStart is index of the first data item that was previously at the top of the viewport
// At the end of a Measure pass _visibleStart == firstViewport.
//
// _visibleCount is the number of data items that were previously visible in the viewport.
int cleanupRangeStart = -1;
int cleanupCount = 0;
int itemIndex = -1; // data item index used to compare with the cache window position.
int lastItemIndex;
IList children = RealizedChildren;
int focusedChild = -1, previousFocusable = -1, nextFocusable = -1; // child indices for the focused item and before and after focus trail items
bool performCleanup = false;
UIElement child;
if (children.Count == 0)
{
return; // nothing to do
}
AdjustCacheWindow(firstViewport, itemsControl.Items.Count);
if (IsKeyboardFocusWithin && !IsPixelBased)
{
// If we're not in a hieararchy we can find the focus trail locally; for hierarchies it has already been
// precalculated.
FindFocusedChild(out focusedChild, out previousFocusable, out nextFocusable);
}
//
// Iterate over all realized children and recycle the ones that are eligible. Items NOT eligible for recycling
// have one or more of the following properties
//
// - inside the cache window
// - the item is its own container
// - has keyboard focus
// - is the first focusable item before or after the focused item
// - the CleanupVirtualizedItem event was canceled
//
for (int childIndex = 0; childIndex < children.Count; childIndex++)
{
child = (UIElement)children[childIndex];
lastItemIndex = itemIndex;
itemIndex = GetGeneratedIndex(childIndex);
if (itemIndex - lastItemIndex != 1)
{
// There's a generated gap between the current item and the last. Clean up the last range of items.
performCleanup = true;
}
if (performCleanup)
{
if (cleanupRangeStart >= 0 && cleanupCount > 0)
{
//
// We've hit a non-virtualizable container or a non-contiguous section.
//
CleanupRange(children, Generator, cleanupRangeStart, cleanupCount);
// CleanupRange just modified the _realizedChildren list. Adjust the childIndex.
childIndex -= cleanupCount;
focusedChild -= cleanupCount;
previousFocusable -= cleanupCount;
nextFocusable -= cleanupCount;
cleanupCount = 0;
cleanupRangeStart = -1;
}
performCleanup = false;
}
if (IsOutsideCacheWindow(itemIndex) &&
!((IGeneratorHost)itemsControl).IsItemItsOwnContainer(itemsControl.Items[itemIndex]) &&
childIndex != focusedChild &&
childIndex != previousFocusable &&
childIndex != nextFocusable &&
!IsInFocusTrail(child) && // logically the same computation as the three above; used when in a treeview.
child != _bringIntoViewContainer && // the container we're going to bring into view must not be recycled
NotifyCleanupItem(child, itemsControl))
{
//
// The container is eligible to be virtualized
//
if (cleanupRangeStart == -1)
{
cleanupRangeStart = childIndex;
}
cleanupCount++;
//
// Save off the child's desired size if we're doing pixel-based virtualization.
// We need to save off the size when doing hierarchical (i.e. TreeView) virtualization, since containers will vary
// greatly in size. This is required both to compute the index of the first visible item in the viewport and to Arrange
// children in their proper locations.
//
if (IsPixelBased)
{
itemsControl.StoreItemValue(itemsControl.Items[itemIndex], child.DesiredSize, _desiredSizeStorageIndex);
}
}
else
{
// Non-recyclable container;
performCleanup = true;
}
}
if (cleanupRangeStart >= 0 && cleanupCount > 0)
{
CleanupRange(children, Generator, cleanupRangeStart, cleanupCount);
}
}
private void EnsureRealizedChildren()
{
Debug.Assert(InRecyclingMode, "This method only applies to recycling mode");
if (_realizedChildren == null)
{
UIElementCollection children = InternalChildren;
_realizedChildren = new List(children.Count);
for (int i = 0; i < children.Count; i++)
{
_realizedChildren.Add(children[i]);
}
}
}
[Conditional("DEBUG")]
private void debug_VerifyRealizedChildren()
{
// Debug method that ensures the _realizedChildren list matches the realized containers in the Generator.
Debug.Assert(IsVirtualizing && InRecyclingMode, "Realized children only exist when recycling");
Debug.Assert(_realizedChildren != null, "Realized children must exist to verify it");
System.Windows.Controls.ItemContainerGenerator generator = Generator as System.Windows.Controls.ItemContainerGenerator;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (generator != null && itemsControl != null && itemsControl.IsGrouping == false)
{
foreach (UIElement child in InternalChildren)
{
int dataIndex = generator.IndexFromContainer(child);
if (dataIndex == -1)
{
// Child is not in the generator's realized container list (i.e. it's a recycled container): ensure it's NOT in _realizedChildren.
Debug.Assert(!_realizedChildren.Contains(child), "_realizedChildren should not contain recycled containers");
}
else
{
// Child is a realized container; ensure it's in _realizedChildren at the proper place.
GeneratorPosition position = Generator.GeneratorPositionFromIndex(dataIndex);
Debug.Assert(_realizedChildren[position.Index] == child, "_realizedChildren is corrupt!");
}
}
}
}
[Conditional("DEBUG")]
private void debug_AssertRealizedChildrenEqualVisualChildren()
{
if (IsVirtualizing && InRecyclingMode)
{
UIElementCollection children = InternalChildren;
Debug.Assert(_realizedChildren.Count == children.Count, "Realized and visual children must match");
for (int i = 0; i < children.Count; i++)
{
Debug.Assert(_realizedChildren[i] == children[i], "Realized and visual children must match");
}
}
}
///
/// Takes an index from the realized list and returns the corresponding index in the Children collection
///
///
///
private int ChildIndexFromRealizedIndex(int realizedChildIndex)
{
//
// If we're not recycling containers then we're not using a realizedChild index and no translation is necessary
//
if (IsVirtualizing && InRecyclingMode)
{
if (realizedChildIndex < _realizedChildren.Count)
{
UIElement child = _realizedChildren[realizedChildIndex];
UIElementCollection children = InternalChildren;
for (int i = realizedChildIndex; i < children.Count; i++)
{
if (children[i] == child)
{
return i;
}
}
Debug.Assert(false, "We should have found a child");
}
}
return realizedChildIndex;
}
///
/// Recycled containers still in the Children collection at the end of Measure should be disconnected
/// from the visual tree. Otherwise they're still visible to things like Arrange, keyboard navigation, etc.
///
private void DisconnectRecycledContainers()
{
int realizedIndex = 0;
UIElement visualChild;
UIElement realizedChild = _realizedChildren.Count > 0 ? _realizedChildren[0] : null;
UIElementCollection children = InternalChildren;
for (int i = 0; i < children.Count; i++)
{
visualChild = children[i];
if (visualChild == realizedChild)
{
realizedIndex++;
if (realizedIndex < _realizedChildren.Count)
{
realizedChild = _realizedChildren[realizedIndex];
}
else
{
realizedChild = null;
}
}
else
{
// The visual child is a recycled container
children.RemoveNoVerify(visualChild);
i--;
}
}
debug_VerifyRealizedChildren();
debug_AssertRealizedChildrenEqualVisualChildren();
}
private GeneratorPosition IndexToGeneratorPositionForStart(int index, out int childIndex)
{
IItemContainerGenerator generator = Generator;
GeneratorPosition position = (generator != null) ? generator.GeneratorPositionFromIndex(index) : new GeneratorPosition(-1, index + 1);
// determine the position in the children collection for the first
// generated container. This assumes that generator.StartAt will be called
// with direction=Forward and allowStartAtRealizedItem=true.
childIndex = (position.Offset == 0) ? position.Index : position.Index + 1;
return position;
}
#region Delayed Cleanup Methods
//
// Delayed Cleanup is used when the VirtualizationMode is standard (not recycling) and the panel is scrolling and item-based
// It chooses to defer virtualizing items until there are enough available. It then cleans them using a background priority dispatcher
// work item
//
private void OnDelayCleanup(object sender, EventArgs e)
{
Debug.Assert(_cleanupDelay != null);
bool needsMoreCleanup = false;
try
{
needsMoreCleanup = CleanUp();
}
finally
{
// Cleanup the timer if more cleanup is unnecessary
if (!needsMoreCleanup)
{
_cleanupDelay.Stop();
_cleanupDelay = null;
}
}
}
private object OnCleanUp(object args)
{
Debug.Assert(_cleanupOperation != null);
bool needsMoreCleanup = false;
try
{
needsMoreCleanup = CleanUp();
}
finally
{
// Keeping this non-null until here in case cleaning up causes re-entrancy
_cleanupOperation = null;
}
if (needsMoreCleanup)
{
EnsureCleanupOperation(true /* delay */);
}
return null;
}
private bool CleanUp()
{
Debug.Assert(!InRecyclingMode, "This method only applies to standard virtualization");
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
if (!IsVirtualizing || !IsItemsHost)
{
// Virtualization is turned off or we aren't hosting children; no need to cleanup.
return false;
}
int startMilliseconds = Environment.TickCount;
bool needsMoreCleanup = false;
UIElementCollection children = InternalChildren;
int minDesiredGenerated = MinDesiredGenerated;
int maxDesiredGenerated = MaxDesiredGenerated;
int pageSize = maxDesiredGenerated - minDesiredGenerated;
int extraChildren = children.Count - pageSize;
if (extraChildren > (pageSize * 2))
{
if ((Mouse.LeftButton == MouseButtonState.Pressed) &&
(extraChildren < 1000))
{
// An optimization for when we are dragging the mouse.
needsMoreCleanup = true;
}
else
{
bool trailingFocus = IsKeyboardFocusWithin;
bool keepForwardTrail = false;
int focusIndex = -1;
IItemContainerGenerator generator = Generator;
int cleanupRangeStart = 0;
int cleanupCount = 0;
int lastGeneratedIndex = -1;
int counterAdjust;
for (int i = 0; i < children.Count; i++)
{
// It is possible for TickCount to wrap around about every 30 days.
// If that were to occur, then this particular cleanup may not be interrupted.
// That is OK since the worst that can happen is that there is more of a stutter than normal.
int totalMilliseconds = Environment.TickCount - startMilliseconds;
if ((totalMilliseconds > 50) && (cleanupCount > 0))
{
// Cleanup has been working for 50ms already and the user might start
// noticing a lag. Stop cleaning up and release the thread for other work.
// Cleanup will continue later.
// Don't break out until after at least one item has been found to cleanup.
// Otherwise, we might end up in an infinite loop.
needsMoreCleanup = true;
break;
}
int childIndex = i;
if (trailingFocus)
{
// Focus lies somewhere within the panel, but it has not been found yet.
UIElement child = children[i];
if (child.IsKeyboardFocusWithin)
{
// Focus has been found, we can now re-virtualize items before the focus.
trailingFocus = false;
keepForwardTrail = true;
focusIndex = i;
if (i > 0)
{
// Go through the trailing items and find a focusable item to keep.
int trailIndex = i - 1;
int end = Math.Max(0, i - FocusTrail);
for (; trailIndex >= end; trailIndex--)
{
child = children[trailIndex];
if (Keyboard.IsFocusable(child))
{
trailIndex--;
break;
}
}
// The rest of the trailing items can be re-virtualized.
for (childIndex = end; childIndex <= trailIndex; childIndex++)
{
ManageCleanup(
children,
itemsControl,
generator,
childIndex,
minDesiredGenerated,
maxDesiredGenerated,
ref childIndex,
ref cleanupRangeStart,
ref cleanupCount,
ref lastGeneratedIndex,
out counterAdjust);
if (counterAdjust > 0)
{
i -= counterAdjust;
trailIndex -= counterAdjust;
}
}
if (cleanupCount > 0)
{
// Cleanup the last batch for the focused item
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
i -= cleanupCount;
cleanupCount = 0;
}
cleanupRangeStart = i + 1;
// At this point, we are caught up and should go to the next item
continue;
}
}
else if (i >= FocusTrail)
{
childIndex = i - FocusTrail;
}
else
{
continue;
}
}
if (keepForwardTrail)
{
// Find a focusable item after the focused item to keep
if (childIndex <= (focusIndex + FocusTrail))
{
UIElement child = children[childIndex];
if (Keyboard.IsFocusable(child))
{
// A focusable item was found, all items after this one can be re-virtualized
keepForwardTrail = false;
cleanupRangeStart = childIndex + 1;
cleanupCount = 0;
}
continue;
}
else
{
keepForwardTrail = false;
}
}
ManageCleanup(
children,
itemsControl,
generator,
childIndex,
minDesiredGenerated,
maxDesiredGenerated,
ref i,
ref cleanupRangeStart,
ref cleanupCount,
ref lastGeneratedIndex,
out counterAdjust);
}
if (cleanupCount > 0)
{
// Cleanup the final batch
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
}
}
}
return needsMoreCleanup;
}
private void ManageCleanup(
UIElementCollection children,
ItemsControl itemsControl,
IItemContainerGenerator generator,
int childIndex,
int minDesiredGenerated,
int maxDesiredGenerated,
ref int counter,
ref int cleanupRangeStart,
ref int cleanupCount,
ref int lastGeneratedIndex,
out int counterAdjust)
{
counterAdjust = 0;
bool performCleanup = false;
bool countThisChild = false;
int generatedIndex = GetGeneratedIndex(childIndex);
if (OutsideMinMax(generatedIndex, minDesiredGenerated, maxDesiredGenerated) &&
NotifyCleanupItem(childIndex, children, itemsControl))
{
// The item can be re-virtualized.
if ((generatedIndex - lastGeneratedIndex) == 1)
{
// Add another to the current batch.
cleanupCount++;
}
else
{
// There was a gap in generated items. Cleanup any from the previous batch.
performCleanup = countThisChild = true;
}
}
else
{
// The item cannot be re-virtualized. Cleanup any from the previous batch.
performCleanup = true;
}
if (performCleanup)
{
// Cleanup a batch of items
if (cleanupCount > 0)
{
CleanupRange(children, generator, cleanupRangeStart, cleanupCount);
counterAdjust = cleanupCount;
counter -= counterAdjust;
childIndex -= counterAdjust;
cleanupCount = 0;
}
if (countThisChild)
{
// The current child was not included in the batch and should be saved for later
cleanupRangeStart = childIndex;
cleanupCount = 1;
}
else
{
// The next child will start the next batch.
cleanupRangeStart = childIndex + 1;
}
}
lastGeneratedIndex = generatedIndex;
}
private bool NotifyCleanupItem(int childIndex, UIElementCollection children, ItemsControl itemsControl)
{
return NotifyCleanupItem(children[childIndex], itemsControl);
}
private bool NotifyCleanupItem(UIElement child, ItemsControl itemsControl)
{
CleanUpVirtualizedItemEventArgs e = new CleanUpVirtualizedItemEventArgs(itemsControl.ItemContainerGenerator.ItemFromContainer(child), child);
e.Source = this;
OnCleanUpVirtualizedItem(e);
return !e.Cancel;
}
private void CleanupRange(IList children, IItemContainerGenerator generator, int startIndex, int count)
{
if (InRecyclingMode)
{
Debug.Assert(startIndex >= 0 && count > 0);
Debug.Assert(children == _realizedChildren, "the given child list must be the _realizedChildren list when recycling");
((IRecyclingItemContainerGenerator)generator).Recycle(new GeneratorPosition(startIndex, 0), count);
// The call to Recycle has caused the ItemContainerGenerator to remove some items
// from its list of realized items; we adjust _realizedChildren to match.
_realizedChildren.RemoveRange(startIndex, count);
}
else
{
// Remove the desired range of children
VirtualizingPanel.RemoveInternalChildRange((UIElementCollection)children, startIndex, count);
generator.Remove(new GeneratorPosition(startIndex, 0), count);
}
AdjustFirstVisibleChildIndex(startIndex, count);
}
#endregion
///
/// Called after 'count' items were removed or recycled from the Generator. _firstVisibleChildIndex is the
/// index of the first visible container. This index isn't exactly the child position in the UIElement collection;
/// it's actually the index of the realized container inside the generator. Since we've just removed some realized
/// containers from the generator (by calling Remove or Recycle), we have to adjust the first visible child index.
///
/// index of the first removed item
/// number of items removed
private void AdjustFirstVisibleChildIndex(int startIndex, int count)
{
// Update the index of the first visible generated child
if (startIndex < _firstVisibleChildIndex)
{
int endIndex = startIndex + count - 1;
if (endIndex < _firstVisibleChildIndex)
{
// The first visible index is after the items that were removed
_firstVisibleChildIndex -= count;
}
else
{
// The first visible index was within the items that were removed
_firstVisibleChildIndex = startIndex;
}
}
}
private static bool OutsideMinMax(int i, int min, int max)
{
return ((i < min) || (i > max));
}
private void EnsureTopCapGenerated(Size layoutSlotSize)
{
// Ensure that a focusable item is generated above the first visible item
// so that keyboard navigation works.
IList children;
_beforeTrail = 0;
if (_visibleStart > 0)
{
children = RealizedChildren;
int childIndex = _firstVisibleChildIndex;
UIElement child;
// At most, we will search FocusTrail number of items for a focusable item
for (; _beforeTrail < FocusTrail; _beforeTrail++)
{
if (PreviousChildIsGenerated(childIndex))
{
// The previous child is already generated, check its focusability
childIndex--;
child = (UIElement)children[childIndex];
}
else
{
// Generate the previous child
child = GeneratePreviousChild(childIndex, layoutSlotSize);
}
if ((child == null) || Keyboard.IsFocusable(child))
{
// Either a focusable item was found, or no child was generated
_beforeTrail++;
break;
}
}
}
}
///
/// Returns the MeasureData we'll be using for computations in MeasureOverride. This updates the viewport offset
/// based on the one set in the MeasureData property prior to the call to MeasureOverride.
///
///
///
///
///
private MeasureData AdjustViewportOffset(MeasureData givenMeasureData, ItemsControl itemsControl, bool isHorizontal)
{
// Note that a panel should not modify its own MeasureData -- it needs to be treated exactly as if it was a variable
// passed into MeasureOverride. That's why we make a copy of MeasureData in this method and return that.
Rect viewport;
MeasureData newMeasureData = null;
IProvideStackingSize stackingSize;
double offset = 0d;
Debug.Assert(MeasureData == null || IsPixelBased, "If a panel has measure data then it must be pixel based");
Debug.Assert(!IsScrolling && IsPixelBased, "This only applies to internal panels");
//
// This panel isn't a scroll owner but some panel above it is. It will be able to use the viewport data
// to virtualize.
//
if (givenMeasureData != null)
{
viewport = givenMeasureData.Viewport;
stackingSize = itemsControl as IProvideStackingSize;
Debug.Assert(givenMeasureData.HasViewport, "MeasureData is only set on objects when we want to pass down viewport information.");
//
// We need to offset the viewport to take into account the delta between the top of the items control
// and this panel (i.e. the header). Ask for the header, and, if not available, use the estimated container size.
if (stackingSize != null)
{
offset = stackingSize.HeaderSize(isHorizontal);
if (offset <= 0d || DoubleUtil.IsNaN(offset))
{
offset = ContainerStackingSizeEstimate(stackingSize, isHorizontal);
}
}
if (isHorizontal)
{
viewport.X -= offset;
}
else
{
// adjust viewport for the header of the TreeViewItem containing this as an ItemsPanel.
viewport.Y -= offset;
}
newMeasureData = new MeasureData(givenMeasureData.AvailableSize, viewport);
}
return newMeasureData;
}
///
/// Sets up IsVirtualizing, VirtualizationMode, and IsPixelBased
///
/// IsVirtualizing is true if turned on via the items control and if the panel has a viewport.
/// VSP has a viewport if it's either the scrolling panel or it was given MeasureData.
///
/// IsPixelBased is true if the panel is virtualizing and (for backwards compat) is the ItemsHost for a TreeView or TreeViewItem.
/// VSP can only make use of, create, and propagate down MeasureData if it is pixel-based, since the viewport is in pixels.
///
///
private void SetVirtualizationState(ItemsControl itemsControl, bool hasMeasureData)
{
VirtualizationMode mode = (itemsControl != null) ? GetVirtualizationMode(itemsControl) : VirtualizationMode.Standard;
if (itemsControl != null)
{
// Set IsVirtualizing. This panel can only virtualize if IsVirtualizing is set on its ItemsControl and it has viewport data.
// It has viewport data if it's either the scroll host or was given viewport information by measureData.
if (GetIsVirtualizing(itemsControl) && (IsScrolling || hasMeasureData))
{
IsVirtualizing = true;
}
}
else
{
IsVirtualizing = false;
}
//
// Set up info on first measure
//
if (HasMeasured)
{
VirtualizationMode oldMode = VirtualizationMode;
if (oldMode != mode)
{
throw new InvalidOperationException(SR.Get(SRID.CantSwitchVirtualizationModePostMeasure));
}
}
else
{
HasMeasured = true;
if (IsVirtualizing && (itemsControl is TreeView || itemsControl is TreeViewItem))
{
IsPixelBased = true;
}
VirtualizationMode = mode;
}
}
private int MinDesiredGenerated
{
get
{
return Math.Max(0, _visibleStart - _beforeTrail);
}
}
private int MaxDesiredGenerated
{
get
{
return Math.Min(ItemCount, _visibleStart + _visibleCount + _afterTrail);
}
}
private int ItemCount
{
get
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
return (itemsControl != null) ? itemsControl.Items.Count : 0;
}
}
#endregion
private void EnsureScrollData()
{
if (_scrollData == null) { _scrollData = new ScrollData(); }
}
private static void ResetScrolling(VirtualizingStackPanel element)
{
element.InvalidateMeasure();
// Clear scrolling data. Because of thrash (being disconnected & reconnected, &c...), we may
if (element.IsScrolling)
{
element._scrollData.ClearLayout();
}
}
// OnScrollChange is an override called whenever the IScrollInfo exposed scrolling state changes on this element.
// At the time this method is called, scrolling state is in its new, valid state.
private void OnScrollChange()
{
if (ScrollOwner != null) { ScrollOwner.InvalidateScrollInfo(); }
}
private void SetAndVerifyScrollingData(Size viewport, Size extent, Vector offset)
{
Debug.Assert(IsScrolling);
if (IsPixelBased)
{
// _scrollData is in pixels and thus operations like LineDown can push the offset too far.
// The behavior here is effectively the same as ScrollContentPresenter.VerifyScrollData
offset.X = ScrollContentPresenter.CoerceOffset(offset.X, extent.Width, viewport.Width);
offset.Y = ScrollContentPresenter.CoerceOffset(offset.Y, extent.Height, viewport.Height);
}
// Detect changes to the viewport, extent, and offset
bool viewportChanged = !DoubleUtil.AreClose(viewport, _scrollData._viewport);
bool extentChanged = !DoubleUtil.AreClose(extent, _scrollData._extent);
bool offsetChanged = !DoubleUtil.AreClose(offset, _scrollData._computedOffset);
// Update data and fire scroll change notifications
_scrollData._offset = offset;
if (viewportChanged || extentChanged || offsetChanged)
{
Vector oldViewportOffset = _scrollData._computedOffset;
Size oldViewportSize = _scrollData._viewport;
_scrollData._viewport = viewport;
_scrollData._extent = extent;
_scrollData._computedOffset = offset;
// Report changes to the viewport
if (viewportChanged)
{
OnViewportSizeChanged(oldViewportSize, viewport);
}
// Report changes to the offset
if (offsetChanged)
{
OnViewportOffsetChanged(oldViewportOffset, offset);
}
OnScrollChange();
}
}
///
/// Allows subclasses to be notified of changes to the viewport size data.
///
/// The old value of the size.
/// The new value of the size.
protected virtual void OnViewportSizeChanged(Size oldViewportSize, Size newViewportSize)
{
}
///
/// Allows subclasses to be notified of changes to the viewport offset data.
///
/// The old value of the offset.
/// The new value of the offset.
protected virtual void OnViewportOffsetChanged(Vector oldViewportOffset, Vector newViewportOffset)
{
}
// Translates a logical (child index) offset to a physical (1/96") when scrolling.
// If virtualizing, it makes the assumption that the logicalOffset is always the first in the visual collection
// and thus returns 0.
// If not virtualizing, it assumes that children are Measure clean; should only be called after running Measure.
private double ComputePhysicalFromLogicalOffset(double logicalOffset, bool fHorizontal)
{
double physicalOffset = 0.0;
IList children = RealizedChildren;
Debug.Assert(logicalOffset == 0 || (logicalOffset > 0 && logicalOffset < children.Count));
for (int i = 0; i < logicalOffset; i++)
{
UIElement child = (UIElement)children[i];
physicalOffset -= (fHorizontal)
? child.DesiredSize.Width
: child.DesiredSize.Height;
}
return physicalOffset;
}
private int FindChildIndexThatParentsVisual(Visual v)
{
DependencyObject child = v;
DependencyObject parent = VisualTreeHelper.GetParent(child);
while (parent != this)
{
child = parent;
parent = VisualTreeHelper.GetParent(child);
}
IList children = RealizedChildren;
for (int i = 0; i < children.Count; i++)
{
if (children[i] == child)
{
return GetGeneratedIndex(i);
}
}
return -1;
}
// This is very similar to the work that ScrollContentPresenter does for MakeVisible. Simply adjust by a
// pixel offset.
private void MakeVisiblePhysicalHelper(Rect r, ref Vector newOffset, ref Rect newRect, bool isHorizontal)
{
double viewportOffset;
double viewportSize;
double targetRectOffset;
double targetRectSize;
double minPhysicalOffset;
if (isHorizontal)
{
viewportOffset = _scrollData._computedOffset.X;
viewportSize = ViewportWidth;
targetRectOffset = r.X;
targetRectSize = r.Width;
}
else
{
viewportOffset = _scrollData._computedOffset.Y;
viewportSize = ViewportHeight;
targetRectOffset = r.Y;
targetRectSize = r.Height;
}
targetRectOffset += viewportOffset;
minPhysicalOffset = ScrollContentPresenter.ComputeScrollOffsetWithMinimalScroll(
viewportOffset, viewportOffset + viewportSize, targetRectOffset, targetRectOffset + targetRectSize);
// Compute the visible rectangle of the child relative to the viewport.
double left = Math.Max(targetRectOffset, minPhysicalOffset);
targetRectSize = Math.Max(Math.Min(targetRectSize + targetRectOffset, minPhysicalOffset + viewportSize) - left, 0);
targetRectOffset = left;
targetRectOffset -= viewportOffset;
if (isHorizontal)
{
newOffset.X = minPhysicalOffset;
newRect.X = targetRectOffset;
newRect.Width = targetRectSize;
}
else
{
newOffset.Y = minPhysicalOffset;
newRect.Y = targetRectOffset;
newRect.Height = targetRectSize;
}
}
private void MakeVisibleLogicalHelper(int childIndex, Rect r, ref Vector newOffset, ref Rect newRect)
{
bool fHorizontal = (Orientation == Orientation.Horizontal);
int firstChildInView;
int newFirstChild;
int viewportSize;
double childOffsetWithinViewport = r.Y;
if (fHorizontal)
{
firstChildInView = (int)_scrollData._computedOffset.X;
viewportSize = (int)_scrollData._viewport.Width;
}
else
{
firstChildInView = (int)_scrollData._computedOffset.Y;
viewportSize = (int)_scrollData._viewport.Height;
}
newFirstChild = firstChildInView;
// If the target child is before the current viewport, move the viewport to put the child at the top.
if (childIndex < firstChildInView)
{
childOffsetWithinViewport = 0;
newFirstChild = childIndex;
}
// If the target child is after the current viewport, move the viewport to put the child at the bottom.
else if (childIndex > firstChildInView + viewportSize - 1)
{
newFirstChild = childIndex - viewportSize + 1;
double pixelSize = fHorizontal ? ActualWidth : ActualHeight;
childOffsetWithinViewport = pixelSize * (1.0 - (1.0 / viewportSize));
}
if (fHorizontal)
{
newOffset.X = newFirstChild;
newRect.X = childOffsetWithinViewport;
newRect.Width = r.Width;
}
else
{
newOffset.Y = newFirstChild;
newRect.Y = childOffsetWithinViewport;
newRect.Height = r.Height;
}
}
// Converts an index into the item collection as a double into an int
static private int CoerceIndexToInteger(double index, int numberOfItems)
{
int newIndex;
if (Double.IsNegativeInfinity(index))
{
newIndex = 0;
}
else if (Double.IsPositiveInfinity(index))
{
newIndex = numberOfItems - 1;
}
else
{
newIndex = (int)index;
newIndex = Math.Max(Math.Min(numberOfItems - 1, newIndex), 0);
}
return newIndex;
}
private int GetGeneratedIndex(int childIndex)
{
return Generator.IndexFromGeneratorPosition(new GeneratorPosition(childIndex, 0));
}
//
// Focus Helpers
//
#region Focus Helpers
//
// Methods to keep track of focus.
//
// Dealing with Focus while virtualizing a list is easy: don't throw away the focused item and the next and previous
// focusable items. When in a TreeView it's much harder; Measure (and thus the cleanup code) for any VSP in the hierarchy
// can run at any time. The only performant way for a panel to know that one of its children may be the next or previous focusable
// item is for it to be marked. We do this every time focus changes within the hierarchy.
//
private WeakReference[] EnsureFocusTrail()
{
WeakReference[] focusTrail = FocusTrailField.GetValue(this);
if (focusTrail == null)
{
focusTrail = new WeakReference[2];
FocusTrailField.SetValue(this, focusTrail);
}
return focusTrail;
}
///
/// Finds the focused child along with the previous and next focusable children. Used only when recycling containers;
/// the standard mode has a different cleanup algorithm
///
///
///
///
private void FindFocusedChild(out int focusedChild, out int previousFocusable, out int nextFocusable)
{
Debug.Assert(InRecyclingMode, "This method is only valid for the recycling mode");
Debug.Assert(IsKeyboardFocusWithin, "we should only search for a focusable child if we have focus");
focusedChild = previousFocusable = nextFocusable = -1;
UIElement child;
bool foundFocusedChild = false;
for (int i = 0; i < _realizedChildren.Count; i++)
{
child = _realizedChildren[i];
if (!foundFocusedChild && child.IsKeyboardFocusWithin)
{
focusedChild = i;
foundFocusedChild = true;
// Go through the trailing items.
// Go through the trailing items and find a focusable item to keep.
int trailIndex = i - 1;
int end = Math.Max(0, i - FocusTrail);
for (; trailIndex >= end; trailIndex--)
{
child = _realizedChildren[trailIndex];
if (Keyboard.IsFocusable(child))
{
previousFocusable = trailIndex;
break;
}
}
}
else if (foundFocusedChild)
{
if (i <= focusedChild + FocusTrail)
{
if (Keyboard.IsFocusable(child))
{
nextFocusable = i;
break;
}
}
else
{
break;
}
}
}
}
///
/// Called when the focused item has changed. Used to set a special DP on the next and previous focusable items.
/// Only used when virtualizing in a hieararchy (i.e. TreeView virtualization).
///
///
private void FocusChanged(KeyboardFocusChangedEventArgs e)
{
if (IsVirtualizing && IsScrolling && IsPixelBased)
{
// IsScrolling ensures that only the top-level panel tracks focus.
// The IsPixelBased condition here needs explanation. It's used here to mean 'Is this panel in a hierarchy?'
// The assert below is just a reminder to modify this code if the meaning changes.
Debug.Assert(ItemsControl.GetItemsOwner(this) is TreeView);
// This code is TreeViewItem-specific, since it has its own focus logic and we can't override UIElement.PredictFocus
TreeViewItem focusedElement = Keyboard.FocusedElement as TreeViewItem;
WeakReference[] focusTrail = EnsureFocusTrail();
//
// Clear the old focus trail items
//
for (int i = 0; i < 2; i++)
{
DependencyObject trailItem = (DependencyObject)(focusTrail[i] != null ? focusTrail[i].Target : null);
if (trailItem != null)
{
FocusTrailItemField.ClearValue(trailItem);
}
}
//
// Set the new focus trail items
//
if (IsKeyboardFocusWithin)
{
DependencyObject previous = null;
DependencyObject next = null;
if (focusedElement != null)
{
if (Orientation == Orientation.Horizontal)
{
previous = focusedElement.InternalPredictFocus(FocusNavigationDirection.Left);
next = focusedElement.InternalPredictFocus(FocusNavigationDirection.Right);
}
else
{
previous = focusedElement.InternalPredictFocus(FocusNavigationDirection.Up);
next = focusedElement.InternalPredictFocus(FocusNavigationDirection.Down);
}
}
if (previous != null)
{
FocusTrailItemField.SetValue(previous, true);
focusTrail[0] = new WeakReference(previous);
}
if (next != null)
{
FocusTrailItemField.SetValue(next, true);
focusTrail[1] = new WeakReference(next);
}
}
else
{
// Focus has left the tree
FocusTrailField.SetValue(this, null);
}
}
}
///
/// Checks the precomputed focus trail. Valid only if we're in a hierararchy.
///
///
///
private bool IsInFocusTrail(UIElement container)
{
if (IsPixelBased)
{
return FocusTrailItemField.GetValue(container) || container.IsKeyboardFocusWithin;
}
else
{
return false;
}
}
#endregion
//------------------------------------------------------------
// Avalon Property Callbacks/Overrides
//-----------------------------------------------------------
#region Avalon Property Callbacks/Overrides
///
///
///
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Since Orientation is so essential to logical scrolling/virtualization, we synchronously check if
// the new value is different and clear all scrolling data if so.
ResetScrolling(d as VirtualizingStackPanel);
}
#endregion
#endregion Private Methods
//-----------------------------------------------------
//
// Private Properties
//
//-----------------------------------------------------
#region Private Properties
///
/// Index of the last item in the cache window
///
private int CacheEnd
{
get
{
// Note we don't have the _afterTrail here: _afterTrail is already contained inside of _visibleCount.
int cacheCount = _beforeTrail + _visibleCount + ContainerCacheSize;
if (cacheCount > 0)
{
return _cacheStart + cacheCount - 1;
}
else
{
return 0;
}
}
}
///
/// True after the first MeasureOverride call. We can't use UIElement.NeverMeasured because it's set to true by the first call to MeasureOverride.
/// Stored in a bool field on Panel.
///
private bool HasMeasured
{
get
{
return VSP_HasMeasured;
}
set
{
VSP_HasMeasured = value;
}
}
private bool InRecyclingMode
{
get
{
return _virtualizationMode == VirtualizationMode.Recycling;
}
}
internal bool IsScrolling
{
get { return (_scrollData != null) && (_scrollData._scrollOwner != null); }
}
///
/// Specifies if this panel uses item-based or pixel-based computations in Measure and Arrange.
///
/// Differences between the two:
///
/// When pixel-based mode VSP behaves the same to the layout system virtualized as not; its desired size is the sum
/// of all its children and it arranges children such that the ones in view appear in the right place.
/// In this mode VSP is also able to make use of the viewport passed down in MeasureData to virtualize chidren. When
/// it's the scrolling panel it computes the offset and extent in pixels rather than logical units.
///
/// When in item mode VSP's desired size grows and shrinks depending on which containers are virtualized and it arranges
/// all children one on top the the other.
/// In this mode VSP cannot use the viewport from MeasureData to virtualize; it can only virtualize if it is the scrolling panel
/// (IsScrolling == true). Thus its looseness with desired size isn't much of an issue since it owns the extent.
///
///
/// This should be private, except that one Debug.Assert in TreeView requires it.
///
internal bool IsPixelBased
{
get
{
// For backwards compat we don't use pixel mode unless we're virtualzing a TreeView or TreeViewItem. This should
// be changed if we decide to later publicly expose the pixel-based viewport.
Debug.Assert(VSP_IsPixelBased == false || IsVirtualizing && (ItemsControl.GetItemsOwner(this) is TreeView || ItemsControl.GetItemsOwner(this) is TreeViewItem));
return VSP_IsPixelBased;
}
set
{
VSP_IsPixelBased = value;
}
}
private bool IsVirtualizing
{
get
{
return VSP_IsVirtualizing;
}
set
{
// We must be the ItemsHost to turn on Virtualization.
bool isVirtualizing = IsItemsHost && value;
if (isVirtualizing == false)
{
_realizedChildren = null;
}
VSP_IsVirtualizing = value;
}
}
///
/// Returns the list of childen that have been realized by the Generator.
/// We must use this method whenever we interact with the Generator's index.
/// In recycling mode the Children collection also contains recycled containers and thus does
/// not map to the Generator's list.
///
private IList RealizedChildren
{
get
{
if (IsVirtualizing && InRecyclingMode)
{
EnsureRealizedChildren();
return _realizedChildren;
}
else
{
return InternalChildren;
}
}
}
private VirtualizationMode VirtualizationMode
{
get
{
return _virtualizationMode;
}
set
{
_virtualizationMode = value;
}
}
#endregion Private Properties
//------------------------------------------------------
//
// Private Fields
//
//-----------------------------------------------------
#region Private Fields
// Scrolling and virtualization data. Only used when this is the scrolling panel (IsScrolling is true).
// When VSP is in pixel mode _scrollData is in units of pixels. Otherwise the units are logical.
private ScrollData _scrollData;
// Virtualization state
private VirtualizationMode _virtualizationMode;
private int _visibleStart; // index of of the first visible data item
private int _visibleCount; // count of the number of data items visible in the viewport
private int _cacheStart; // index of the first data item in the container cache. This is always <= _visibleStart
// UIElement collection index of the first visible child container. This is NOT the data item index. If the first visible container
// is the 3rd child in the visual tree and contains data item 312, _firstVisibleChildIndex will be 2, while _visibleStart is 312.
// This is useful because could be several live containers in the collection offscreen (maybe we cleaned up lazily, they couldn't be virtualized, etc).
// This actually maps directly to realized containers inside the Generator. It's the index of the first visible realized container.
// Note that when RecyclingMode is active this is the index into the _realizedChildren collection, not the Children collection.
private int _firstVisibleChildIndex;
// Used by the Recycling mode to maintain the list of actual realized children (a realized child is one that the ItemContainerGenerator has
// generated). We need a mapping between children in the UIElementCollection and realized containers in the generator. In standard virtualization
// mode these lists are identical; in recycling mode they are not. When a container is recycled the Generator removes it from its realized list, but
// for perf reasons the panel keeps these containers in its UIElement collection. This list is the actual realized children -- i.e. the InternalChildren
// list minus all recycled containers.
private List _realizedChildren;
// Cleanup
private DispatcherOperation _cleanupOperation;
private DispatcherTimer _cleanupDelay;
private int _beforeTrail = 0;
private int _afterTrail = 0;
private const int FocusTrail = 5; // The maximum number of items off the edge we will generate to get a focused item (so that keyboard navigation can work)
private DependencyObject _bringIntoViewContainer; // pointer to the container we're about to bring into view; it can't be recycled even if it's offscreen.
// ContainerCacheSize specifies how many items we cache past the viewport boundaries. Until we expose an API to allow users to tweak this
// the safest thing is to leave it at 0.
private const int ContainerCacheSize = 0;
// Global index used by ItemValueStorage to store the DesiredSize of a UIElement when it is a virtualized container.
// Used by TreeView and TreeViewItem to remember the size of TreeViewItems when they get virtualized away.
private static int _desiredSizeStorageIndex;
// Holds the 'focus trail': the previous or next focusable item, neither of which can be virtualized.
// Used only when virtualizing in a hierarchy (i.e. TreeView virtualization).
private static UncommonField FocusTrailField = new UncommonField(null);
private static UncommonField FocusTrailItemField = new UncommonField(false);
#endregion Private Fields
//------------------------------------------------------
//
// Private Structures / Classes
//
//------------------------------------------------------
#region Private Structures Classes
//-----------------------------------------------------------
// ScrollData class
//------------------------------------------------------------
#region ScrollData
// Helper class to hold scrolling data.
// This class exists to reduce working set when VirtualizingStackPanel is used outside a scrolling situation.
// Standard "extra pointer always for less data sometimes" cache savings model:
// !Scroll [1xReference]
// Scroll [1xReference] + [6xDouble + 1xReference]
private class ScrollData
{
// Clears layout generated data.
// Does not clear scrollOwner, because unless resetting due to a scrollOwner change, we won't get reattached.
internal void ClearLayout()
{
_offset = new Vector();
_viewport = _extent = _maxDesiredSize = new Size();
}
// For Stack/Flow, the two dimensions of properties are in different units:
// 1. The "logically scrolling" dimension uses items as units.
// 2. The other dimension physically scrolls. Units are in Avalon pixels (1/96").
internal bool _allowHorizontal;
internal bool _allowVertical;
// Scroll offset of content. Positive corresponds to a visually upward offset. Set by methods like LineUp, PageDown, etc.
internal Vector _offset;
// Computed offset based on _offset set by the IScrollInfo methods. Set at the end of a successful Measure pass.
// This is the offset used by Arrange and exposed externally. Thus an offset set by PageDown via IScrollInfo isn't
// reflected publicly (e.g. via the VerticalOffset property) until a Measure pass.
internal Vector _computedOffset = new Vector(0,0);
internal Size _viewport; // ViewportSize is in {pixels x items} (or vice-versa).
internal Size _extent; // Extent is the total number of children (logical dimension) or physical size
internal ScrollViewer _scrollOwner; // ScrollViewer to which we're attached.
internal Size _maxDesiredSize; // Hold onto the maximum desired size to avoid re-laying out the parent ScrollViewer.
}
#endregion ScrollData
///
/// Allows pixel-based virtualization to ask an ItemsControl for the size of its header (if available)
/// and a size estimate for its containers. This is used for TreeView virtualization.
///
///
internal interface IProvideStackingSize
{
double HeaderSize(bool isHorizontal);
double EstimatedContainerSize(bool isHorizontal);
}
#endregion Private Structures Classes
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- DictionaryCustomTypeDescriptor.cs
- PropertyInfoSet.cs
- PerspectiveCamera.cs
- SessionParameter.cs
- BitmapInitialize.cs
- TextViewDesigner.cs
- FormViewPagerRow.cs
- ipaddressinformationcollection.cs
- InternalCache.cs
- GCHandleCookieTable.cs
- ScriptResourceAttribute.cs
- PlanCompiler.cs
- PageContentAsyncResult.cs
- SqlDataSourceAdvancedOptionsForm.cs
- HttpRuntimeSection.cs
- CategoryState.cs
- MultiAsyncResult.cs
- TemplateKey.cs
- LocalizableAttribute.cs
- DynamicControlParameter.cs
- WebEventCodes.cs
- Action.cs
- ApplicationContext.cs
- FastPropertyAccessor.cs
- BinaryFormatterSinks.cs
- OdbcParameterCollection.cs
- BulletedListEventArgs.cs
- HGlobalSafeHandle.cs
- IndependentAnimationStorage.cs
- ActivityPropertyReference.cs
- ToolBarOverflowPanel.cs
- ThreadAttributes.cs
- WindowsListViewItemStartMenu.cs
- SoapSchemaImporter.cs
- ColumnResizeAdorner.cs
- SemanticValue.cs
- TemplateBindingExtension.cs
- LocalizableAttribute.cs
- IntSecurity.cs
- DataTransferEventArgs.cs
- DispatcherHookEventArgs.cs
- DataFormat.cs
- ContentDisposition.cs
- OutputCacheSection.cs
- AQNBuilder.cs
- Function.cs
- QilTernary.cs
- APCustomTypeDescriptor.cs
- MailAddressCollection.cs
- InternalRelationshipCollection.cs
- DbResourceAllocator.cs
- KnownBoxes.cs
- XmlSchemaImporter.cs
- DataChangedEventManager.cs
- FontUnit.cs
- FontNamesConverter.cs
- FileAuthorizationModule.cs
- OracleConnectionFactory.cs
- CmsUtils.cs
- ConfigurationErrorsException.cs
- TableHeaderCell.cs
- FrameworkTemplate.cs
- SerTrace.cs
- ActivationProxy.cs
- WebPageTraceListener.cs
- XmlUtilWriter.cs
- StringDictionaryWithComparer.cs
- Attributes.cs
- LoadedEvent.cs
- GCHandleCookieTable.cs
- SizeAnimationClockResource.cs
- WindowsTitleBar.cs
- DataGridDetailsPresenterAutomationPeer.cs
- StructuredTypeInfo.cs
- XmlSchemaSimpleContentExtension.cs
- Base64Stream.cs
- MatrixValueSerializer.cs
- CodeAssignStatement.cs
- SafeTimerHandle.cs
- DragDeltaEventArgs.cs
- UnsafeNativeMethods.cs
- ProxySimple.cs
- MasterPage.cs
- PageContentCollection.cs
- MainMenu.cs
- HyperLinkColumn.cs
- DataTableNewRowEvent.cs
- documentsequencetextview.cs
- TagMapInfo.cs
- LogicalTreeHelper.cs
- SharedStatics.cs
- BindingMAnagerBase.cs
- FrugalMap.cs
- TrayIconDesigner.cs
- ByteKeyFrameCollection.cs
- SQLInt16.cs
- WindowsRegion.cs
- AmbiguousMatchException.cs
- RijndaelManagedTransform.cs
- XmlException.cs