diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/HierarchicalVirtualizationConstraints.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/HierarchicalVirtualizationConstraints.cs index 31d28ed1001..25bf9dbe67a 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/HierarchicalVirtualizationConstraints.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/HierarchicalVirtualizationConstraints.cs @@ -21,6 +21,7 @@ public HierarchicalVirtualizationConstraints(VirtualizationCacheLength cacheLeng _cacheLength = cacheLength; _cacheLengthUnit = cacheLengthUnit; _viewport = viewport; + _scrollGeneration = 0; // internal field set separately by caller } #endregion @@ -131,11 +132,22 @@ public override int GetHashCode() #endregion + #region Internal properties + + internal long ScrollGeneration + { + get { return _scrollGeneration; } + set { _scrollGeneration = value; } + } + + #endregion + #region Data VirtualizationCacheLength _cacheLength; VirtualizationCacheLengthUnit _cacheLengthUnit; Rect _viewport; + long _scrollGeneration; #endregion } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/VirtualizingStackPanel.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/VirtualizingStackPanel.cs index 269b64e1cf5..d85ce7355cd 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/VirtualizingStackPanel.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/VirtualizingStackPanel.cs @@ -511,6 +511,8 @@ private void SetHorizontalOffsetImpl(double offset, bool setAnchorInformation) // offset/extent, which ends up scrolling to a "random" place. if (!IsVSP45Compat && Orientation == Orientation.Horizontal) { + IncrementScrollGeneration(); + double delta = Math.Abs(scrollX - oldViewportOffset.X); if (DoubleUtil.LessThanOrClose(delta, ViewportWidth)) { @@ -637,6 +639,8 @@ private void SetVerticalOffsetImpl(double offset, bool setAnchorInformation) // offset/extent, which ends up scrolling to a "random" place. if (!IsVSP45Compat && Orientation == Orientation.Vertical) { + IncrementScrollGeneration(); + double delta = Math.Abs(scrollY - oldViewportOffset.Y); if (DoubleUtil.LessThanOrClose(delta, ViewportHeight)) { @@ -980,9 +984,19 @@ private void OnAnchorOperation(bool isAnchorOperationPending) if (DoubleUtil.LessThan(expectedOffset, 0) || DoubleUtil.GreaterThan(expectedOffset, _scrollData._extent.Width - _scrollData._viewport.Width)) { - Debug.Assert(DoubleUtil.AreClose(actualOffset, 0) || DoubleUtil.AreClose(actualOffset, _scrollData._extent.Width - _scrollData._viewport.Width), "The actual offset should already be at the beginning or the end."); - _scrollData._computedOffset.X = actualOffset; - _scrollData._offset.X = actualOffset; + // the condition can fail due to estimated sizes in subtrees that contribute + // to FindScrollOffset(_scrollData._firstContainerInViewport) but not to + // _scrollData._extent. If that happens, remeasure. + if (DoubleUtil.AreClose(actualOffset, 0) || DoubleUtil.AreClose(actualOffset, _scrollData._extent.Width - _scrollData._viewport.Width)) + { + _scrollData._computedOffset.X = actualOffset; + _scrollData._offset.X = actualOffset; + } + else + { + remeasure = true; + _scrollData._offset.X = expectedOffset; + } } else { @@ -999,9 +1013,19 @@ private void OnAnchorOperation(bool isAnchorOperationPending) if (DoubleUtil.LessThan(expectedOffset, 0) || DoubleUtil.GreaterThan(expectedOffset, _scrollData._extent.Height - _scrollData._viewport.Height)) { - Debug.Assert(DoubleUtil.AreClose(actualOffset, 0) || DoubleUtil.AreClose(actualOffset, _scrollData._extent.Height - _scrollData._viewport.Height), "The actual offset should already be at the beginning or the end."); - _scrollData._computedOffset.Y = actualOffset; - _scrollData._offset.Y = actualOffset; + // the condition can fail due to estimated sizes in subtrees that contribute + // to FindScrollOffset(_scrollData._firstContainerInViewport) but not to + // _scrollData._extent. If that happens, remeasure. + if (DoubleUtil.AreClose(actualOffset, 0) || DoubleUtil.AreClose(actualOffset, _scrollData._extent.Height - _scrollData._viewport.Height)) + { + _scrollData._computedOffset.Y = actualOffset; + _scrollData._offset.Y = actualOffset; + } + else + { + remeasure = true; + _scrollData._offset.Y = expectedOffset; + } } else { @@ -1027,6 +1051,9 @@ private void OnAnchorOperation(bool isAnchorOperationPending) if (!isVSP45Compat) { CancelPendingAnchoredInvalidateMeasure(); + + // remeasure from the root should use fresh effective offsets + IncrementScrollGeneration(); } if (!isAnchorOperationPending) @@ -2149,6 +2176,7 @@ private Size MeasureOverrideImpl(Size constraint, // The viewport constraint used by this panel. // Rect viewport = Rect.Empty, extendedViewport = Rect.Empty; + long scrollGeneration; // // Sizes of cache before/after viewport @@ -2159,7 +2187,7 @@ private Size MeasureOverrideImpl(Size constraint, // // Initialize the viewport for this panel. // - InitializeViewport(parentItem, parentItemStorageProvider, virtualizationInfoProvider, isHorizontal, constraint, ref viewport, ref cacheSize, ref cacheUnit, out extendedViewport); + InitializeViewport(parentItem, parentItemStorageProvider, virtualizationInfoProvider, isHorizontal, constraint, ref viewport, ref cacheSize, ref cacheUnit, out extendedViewport, out scrollGeneration); // =================================================================================== // =================================================================================== @@ -2401,6 +2429,7 @@ private Size MeasureOverrideImpl(Size constraint, ref viewport, ref cacheSize, ref cacheUnit, + ref scrollGeneration, ref foundFirstItemInViewport, ref firstItemInViewportOffset, ref stackPixelSize, @@ -2500,6 +2529,7 @@ private Size MeasureOverrideImpl(Size constraint, ref viewport, ref cacheSize, ref cacheUnit, + ref scrollGeneration, ref foundFirstItemInViewport, ref firstItemInViewportOffset, ref stackPixelSize, @@ -2702,6 +2732,7 @@ private Size MeasureOverrideImpl(Size constraint, ref viewport, ref cacheSize, ref cacheUnit, + ref scrollGeneration, ref foundFirstItemInViewport, ref firstItemInViewportOffset, ref stackPixelSize, @@ -2789,9 +2820,10 @@ private Size MeasureOverrideImpl(Size constraint, ref firstItemInViewportOffset, ref mustDisableVirtualization, ref hasVirtualizingChildren, - ref hasBringIntoViewContainerBeenMeasured); + ref hasBringIntoViewContainerBeenMeasured, + ref scrollGeneration); - if (ItemsChangedDuringMeasure) + if (ItemsChangedDuringMeasure) { // if the Items collection changed, our state is now invalid. Start over. remeasure = true; @@ -2837,9 +2869,10 @@ private Size MeasureOverrideImpl(Size constraint, ref firstItemInViewportOffset, ref mustDisableVirtualization, ref hasVirtualizingChildren, - ref hasBringIntoViewContainerBeenMeasured); + ref hasBringIntoViewContainerBeenMeasured, + ref scrollGeneration); - if (ItemsChangedDuringMeasure) + if (ItemsChangedDuringMeasure) { // if the Items collection changed, our state is now invalid. Start over. remeasure = true; @@ -3043,7 +3076,8 @@ private Size MeasureOverrideImpl(Size constraint, virtualizationInfoProvider, isHorizontal, areContainersUniformlySized, - uniformOrAverageContainerSize); + uniformOrAverageContainerSize, + scrollGeneration); // also revise the offset of the first container, for use in Arrange if (firstContainerInViewport != null) @@ -3084,7 +3118,8 @@ private Size MeasureOverrideImpl(Size constraint, ref viewport, firstContainerInViewport, firstItemInViewportIndex, - firstItemInViewportOffset); + firstItemInViewportOffset, + scrollGeneration); FirstContainerInformationField.SetValue(this, info); } } @@ -3167,10 +3202,11 @@ private Size MeasureOverrideImpl(Size constraint, { // save information needed by Snapshot DependencyObject offsetHost = virtualizationInfoProvider as DependencyObject; + EffectiveOffsetInformation effectiveOffsetInfo = (offsetHost != null) ? EffectiveOffsetInformationField.GetValue(offsetHost) : null; SnapshotData data = new SnapshotData { UniformOrAverageContainerSize = uniformOrAverageContainerPixelSize, UniformOrAverageContainerPixelSize = uniformOrAverageContainerPixelSize, - EffectiveOffsets = (offsetHost != null) ? EffectiveOffsetInformationField.GetValue(offsetHost) : null + EffectiveOffsets = (effectiveOffsetInfo != null) ? effectiveOffsetInfo.OffsetList : null }; SnapshotDataField.SetValue(this, data); } @@ -3192,6 +3228,12 @@ private Size MeasureOverrideImpl(Size constraint, if (remeasure) { + if (!IsVSP45Compat && IsScrolling) + { + // remeasure from the root should use fresh effective offsets + IncrementScrollGeneration(); + } + // // Make another pass of MeasureOverride if remeasure is true. // @@ -3460,10 +3502,11 @@ protected override Size ArrangeOverride(Size arrangeSize) { // save information needed by Snapshot DependencyObject offsetHost = virtualizationInfoProvider as DependencyObject; + EffectiveOffsetInformation effectiveOffsetInfo = (offsetHost != null) ? EffectiveOffsetInformationField.GetValue(offsetHost) : null; SnapshotData data = new SnapshotData { UniformOrAverageContainerSize = uniformOrAverageContainerPixelSize, UniformOrAverageContainerPixelSize = uniformOrAverageContainerPixelSize, - EffectiveOffsets = (offsetHost != null) ? EffectiveOffsetInformationField.GetValue(offsetHost) : null + EffectiveOffsets = (effectiveOffsetInfo != null) ? effectiveOffsetInfo.OffsetList : null }; SnapshotDataField.SetValue(this, data); @@ -3810,7 +3853,8 @@ private void UpdateExtent(bool areItemChangesLocal) virtualizationInfoProvider, isHorizontal, areContainersUniformlySized, - uniformOrAverageContainerSize); + uniformOrAverageContainerSize, + info.ScrollGeneration); } } } @@ -4231,7 +4275,8 @@ private void InitializeViewport( ref Rect viewport, ref VirtualizationCacheLength cacheSize, ref VirtualizationCacheLengthUnit cacheUnit, - out Rect extendedViewport) + out Rect extendedViewport, + out long scrollGeneration) { Size extent = new Size(); bool isVSP45Compat = IsVSP45Compat; @@ -4251,6 +4296,7 @@ private void InitializeViewport( offsetY = _scrollData._offset.Y; extent = _scrollData._extent; viewportSize = _scrollData._viewport; + scrollGeneration = _scrollData._scrollGeneration; if (!IsScrollActive || IgnoreMaxDesiredSize) { @@ -4413,6 +4459,7 @@ private void InitializeViewport( viewport = virtualizationConstraints.Viewport; cacheSize = virtualizationConstraints.CacheLength; cacheUnit = virtualizationConstraints.CacheLengthUnit; + scrollGeneration = virtualizationConstraints.ScrollGeneration; MeasureCaches = virtualizationInfoProvider.InBackgroundLayout; if (isVSP45Compat) @@ -4436,34 +4483,46 @@ private void InitializeViewport( // system. // This replacement stays in effect until the parent panel gives us // an offset from a more recent coordinate change, after which older - // offsets won't appear again. Or an offset that's not on the - // list at all, which means a new scroll motion has started. + // offsets won't appear again. Or until a new scroll motion has started, + // as indicated by a scroll generation that exceeds the one in effect + // when the list was created. DependencyObject container = virtualizationInfoProvider as DependencyObject; - List offsetList = EffectiveOffsetInformationField.GetValue(container); - if (offsetList != null) + EffectiveOffsetInformation effectiveOffsetInfo = EffectiveOffsetInformationField.GetValue(container); + if (effectiveOffsetInfo != null) { - // find the given offset on the list - double offset = isHorizontal ? viewport.X : viewport.Y; + List offsetList = effectiveOffsetInfo.OffsetList; int index = -1; - for (int i=0, n=offsetList.Count; i= scrollGeneration) { - if (LayoutDoubleUtil.AreClose(offset, offsetList[i])) + // find the given offset on the list + double offset = isHorizontal ? viewport.X : viewport.Y; + for (int i = 0, n = offsetList.Count; i < n; ++i) { - index = i; - break; + if (LayoutDoubleUtil.AreClose(offset, offsetList[i])) + { + index = i; + break; + } } } if (ScrollTracer.IsEnabled && ScrollTracer.IsTracing(this)) { - object[] args = new object[offsetList.Count + 4]; - args[0] = viewport.Location; - args[1] = "at"; - args[2] = index; - args[3] = "in"; + object[] args = new object[offsetList.Count + 7]; + args[0] = "gen"; + args[1] = effectiveOffsetInfo.ScrollGeneration; + args[2] = virtualizationConstraints.ScrollGeneration; + args[3] = viewport.Location; + args[4] = "at"; + args[5] = index; + args[6] = "in"; for (int i=0; i childOffsetList = EffectiveOffsetInformationField.GetValue(firstContainer); + EffectiveOffsetInformation effectiveOffsetInformation = EffectiveOffsetInformationField.GetValue(firstContainer); + List childOffsetList = (effectiveOffsetInformation != null) ? effectiveOffsetInformation.OffsetList : null; if (childOffsetList != null) { int count = childOffsetList.Count; @@ -5586,32 +5648,51 @@ private double ComputeEffectiveOffset( // multiple calls to measure this panel before the parent // adjusts to the change in our coordinate system, or calls from // a parent who set its own offset using an older offset from here - List offsetList = EffectiveOffsetInformationField.GetValue(container); - if (offsetList == null) + effectiveOffsetInformation = EffectiveOffsetInformationField.GetValue(container); + if (effectiveOffsetInformation == null || effectiveOffsetInformation.ScrollGeneration != scrollGeneration) { - offsetList = new List(2); - offsetList.Add(oldOffset); + effectiveOffsetInformation = new EffectiveOffsetInformation(scrollGeneration); + effectiveOffsetInformation.OffsetList.Add(oldOffset); } - offsetList.Add(newOffset); + effectiveOffsetInformation.OffsetList.Add(newOffset); if (ScrollTracer.IsEnabled && ScrollTracer.IsTracing(this)) { - object[] args = new object[offsetList.Count]; - for (int i=0; i offsetList = effectiveOffsetInformation.OffsetList; + object[] args = new object[offsetList.Count + 2]; + args[0] = scrollGeneration; + args[1] = ":"; + for (int i = 0; i < offsetList.Count; ++i) { - args[i] = offsetList[i]; + args[i + 2] = offsetList[i]; } ScrollTracer.Trace(this, ScrollTraceOp.StoreSubstOffset, args); } - EffectiveOffsetInformationField.SetValue(container, offsetList); + EffectiveOffsetInformationField.SetValue(container, effectiveOffsetInformation); } return newOffset; } + /// + /// To distinguish effective offsets set during one scrolling operation + /// from those set in a different, each scrolling operation in the + /// virtualizing direction increments the "scroll generation" counter. + /// This counter is saved along with the effective offsets (see + /// ComputeEffectiveOffsets), and compared with the current counter + /// before applying the effective offset (see InitializeViewport). + /// + private void IncrementScrollGeneration() + { + // This will break if the counter ever rolls over the maximum. + // If you do 1000 scroll operations per second, that will + // happen in about 280 million years. + ++_scrollData._scrollGeneration; + } + /// /// DesiredSize is normally computed by summing up the size of all items we've generated. Pixel-based virtualization uses a 'full' desired size. @@ -6559,6 +6640,7 @@ private void SetViewportForChild( Rect parentViewport, VirtualizationCacheLength parentCacheSize, VirtualizationCacheLengthUnit parentCacheUnit, + long scrollGeneration, Size stackPixelSize, Size stackPixelSizeInViewport, Size stackPixelSizeInCacheBeforeViewport, @@ -6671,10 +6753,12 @@ private void SetViewportForChild( if (virtualizingChild != null) { - virtualizingChild.Constraints = new HierarchicalVirtualizationConstraints( + HierarchicalVirtualizationConstraints constraints = new HierarchicalVirtualizationConstraints( childCacheSize, childCacheUnit, childViewport); + constraints.ScrollGeneration = scrollGeneration; + virtualizingChild.Constraints = constraints; virtualizingChild.InBackgroundLayout = MeasureCaches; virtualizingChild.MustDisableVirtualization = mustDisableVirtualization; } @@ -7655,6 +7739,20 @@ private void SyncUniformSizeFlags( if (numContainerSizes > 0) { uniformOrAverageContainerPixelSize = sumOfContainerPixelSizes / numContainerSizes; + + if (UseLayoutRounding) + { + // apply layout rounding to the average size, so that anchored + // scrolls use rounded sizes throughout. Otherwise they can + // hang because of rounding done in layout that isn't accounted + // for in OnAnchor. + DpiScale dpi = GetDpi(); + double dpiScale = isHorizontal ? dpi.DpiScaleX : dpi.DpiScaleY; + uniformOrAverageContainerPixelSize = RoundLayoutValue( + Math.Max(uniformOrAverageContainerPixelSize, dpiScale), // don't round down to 0 + dpiScale); + } + if (IsPixelBased) { uniformOrAverageContainerSize = uniformOrAverageContainerPixelSize; @@ -7938,7 +8036,8 @@ private void MeasureExistingChildBeyondExtendedViewport( ref double firstItemInViewportOffset, ref bool mustDisableVirtualization, ref bool hasVirtualizingChildren, - ref bool hasBringIntoViewContainerBeenMeasured) + ref bool hasBringIntoViewContainerBeenMeasured, + ref long scrollGeneration) { object item = ((ItemContainerGenerator)generator).ItemFromContainer((UIElement)children[childIndex]); Rect viewport = new Rect(); @@ -7978,6 +8077,7 @@ private void MeasureExistingChildBeyondExtendedViewport( ref viewport, ref cacheSize, ref cacheUnit, + ref scrollGeneration, ref foundFirstItemInViewport, ref firstItemInViewportOffset, ref stackPixelSize, @@ -8018,6 +8118,7 @@ private void MeasureChild( ref Rect viewport, ref VirtualizationCacheLength cacheSize, ref VirtualizationCacheLengthUnit cacheUnit, + ref long scrollGeneration, ref bool foundFirstItemInViewport, ref double firstItemInViewportOffset, ref Size stackPixelSize, @@ -8089,6 +8190,7 @@ private void MeasureChild( viewport, cacheSize, cacheUnit, + scrollGeneration, stackPixelSize, stackPixelSizeInViewport, stackPixelSizeInCacheBeforeViewport, @@ -11589,7 +11691,7 @@ private enum BoolField : byte private static readonly UncommonField AnchoredInvalidateMeasureOperationField = new UncommonField(); private static readonly UncommonField ClearIsScrollActiveOperationField = new UncommonField(); private static readonly UncommonField OffsetInformationField = new UncommonField(); - private static readonly UncommonField> EffectiveOffsetInformationField = new UncommonField>(); + private static readonly UncommonField EffectiveOffsetInformationField = new UncommonField(); private static readonly UncommonField SnapshotDataField = new UncommonField(); #endregion @@ -11668,6 +11770,9 @@ internal bool IsEmpty internal double _firstContainerOffsetFromViewport; internal double _expectedDistanceBetweenViewports; + // scroll generation - for effective offsets + internal long _scrollGeneration; + public Vector Offset { get @@ -11780,13 +11885,15 @@ private class FirstContainerInformation public DependencyObject FirstContainer; // first container visible in viewport public int FirstItemIndex; // index of corresponding item public double FirstItemOffset; // offset from top of viewport + public long ScrollGeneration; // current scroll generation - public FirstContainerInformation(ref Rect viewport, DependencyObject firstContainer, int firstItemIndex, double firstItemOffset) + public FirstContainerInformation(ref Rect viewport, DependencyObject firstContainer, int firstItemIndex, double firstItemOffset, long scrollGeneration) { Viewport = viewport; FirstContainer = firstContainer; FirstItemIndex = firstItemIndex; FirstItemOffset = firstItemOffset; + ScrollGeneration = scrollGeneration; } } @@ -11833,6 +11940,19 @@ public Double ItemSize } } + // Info needed to support Effective Offsets + private class EffectiveOffsetInformation + { + public long ScrollGeneration { get; private set; } + public List OffsetList { get; private set; } + + public EffectiveOffsetInformation(long scrollGeneration) + { + ScrollGeneration = scrollGeneration; + OffsetList = new List(2); + } + } + #endregion Information caches #region ScrollTracer