I'm trying to create a view that looks like a bookshelf with items sitting on the shelf with virtualization. I was able to achieve this by adding acustom VirtualizingPanel class to my ListView. My custom VirtualizingPanel class looks like this:
using System.Windows.Controls.Primitives; using System.Windows.Controls; using System.Windows; using System.Windows.Media; using System; using System.Diagnostics; using System.Collections.Specialized; using System.Collections.ObjectModel; using System.Collections.Generic; using System.Windows.Media.Imaging; namespace CollectorsWindows { class VirtualizingTilePanel : VirtualizingPanel, IScrollInfo { public VirtualizingTilePanel() { // For use in the IScrollInfo implementation this.RenderTransform = _trans; } // Dependency property that controls the size of the child elements public static readonly DependencyProperty ChildSizeProperty = DependencyProperty.RegisterAttached("ChildSize", typeof(double), typeof(VirtualizingTilePanel), new FrameworkPropertyMetadata(168.0d, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange)); // Accessor for the child size dependency property public double ChildSize { get { return (double)GetValue(ChildSizeProperty); } set { SetValue(ChildSizeProperty, value); } } /// <summary> /// Measure the children /// </summary> /// <param name="availableSize">Size available</param> /// <returns>Size desired</returns> IItemContainerGenerator generator; protected override Size MeasureOverride(Size availableSize) { UpdateScrollInfo(availableSize); // Figure out range that's visible based on layout algorithm int firstVisibleItemIndex, lastVisibleItemIndex; GetVisibleRange(out firstVisibleItemIndex, out lastVisibleItemIndex); // We need to access InternalChildren before the generator to work around a bug UIElementCollection children = this.InternalChildren; generator = this.ItemContainerGenerator; // Get the generator position of the first visible data item GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex); // Get index where we'd insert the child for this position. If the item is realized // (position.Offset == 0), it's just position.Index, otherwise we have to add one to // insert after the corresponding child int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1; using (generator.StartAt(startPos, GeneratorDirection.Forward, true)) { for (int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; ++itemIndex, ++childIndex) { bool newlyRealized; // Get or create the child UIElement child = generator.GenerateNext(out newlyRealized) as UIElement; if (newlyRealized) { // Figure out if we need to insert the child at the end or somewhere in the middle if (childIndex >= children.Count) { base.AddInternalChild(child); } else { base.InsertInternalChild(childIndex, child); } generator.PrepareItemContainer(child); } else { // The child has already been created, let's be sure it's in the right spot Debug.Assert(child == children[childIndex], "Wrong child was generated"); } // Measurements will depend on layout algorithm child.Measure(GetChildSize()); } } // Note: this could be deferred to idle time for efficiency CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex); return availableSize; } /// <summary> /// Arrange the children /// </summary> /// <param name="finalSize">Size available</param> /// <returns>Size used</returns> protected override Size ArrangeOverride(Size finalSize) { IItemContainerGenerator generator = this.ItemContainerGenerator; UpdateScrollInfo(finalSize); for (int i = 0; i < this.Children.Count; i++) { UIElement child = this.Children[i]; // Map the child offset to an item offset int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0)); ArrangeChild(itemIndex, child, finalSize); } return finalSize; } /// <summary> /// Revirtualize items that are no longer visible /// </summary> /// <param name="minDesiredGenerated">first item index that should be visible</param> /// <param name="maxDesiredGenerated">last item index that should be visible</param> private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated) { UIElementCollection children = this.InternalChildren; IItemContainerGenerator generator = this.ItemContainerGenerator; for (int i = children.Count - 1; i >= 0; i--) { GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0); int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos); if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated) { generator.Remove(childGeneratorPos, 1); RemoveInternalChildRange(i, 1); } } } /// <summary> /// When items are removed, remove the corresponding UI if necessary /// </summary> /// <param name="sender"></param> /// <param name="args"></param> protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: RemoveInternalChildRange(args.Position.Index, args.ItemUICount); break; } } #region Layout specific code // I've isolated the layout specific code to this region. If you want to do something other than tiling, this is // where you'll make your changes int width = 100; int height = 203; /// <summary> /// Calculate the extent of the view based on the available size /// </summary> /// <param name="availableSize">available size</param> /// <param name="itemCount">number of data items</param> /// <returns></returns> private Size CalculateExtent(Size availableSize, int itemCount) { int childrenPerRow = CalculateChildrenPerRow(availableSize); // See how big we are return new Size(childrenPerRow * width, height * Math.Ceiling((double)itemCount / childrenPerRow)); } /// <summary> /// Get the range of children that are visible /// </summary> /// <param name="firstVisibleItemIndex">The item index of the first visible item</param> /// <param name="lastVisibleItemIndex">The item index of the last visible item</param> private void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex) { int childrenPerRow = CalculateChildrenPerRow(_extent); firstVisibleItemIndex = (int) Math.Floor(_offset.Y / height) * childrenPerRow; lastVisibleItemIndex = (int) Math.Ceiling((_offset.Y + _viewport.Height) / height) * childrenPerRow - 1; ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0; if (lastVisibleItemIndex >= itemCount) lastVisibleItemIndex = itemCount-1; } /// <summary> /// Get the size of the children. We assume they are all the same /// </summary> /// <returns>The size</returns> private Size GetChildSize() { //return new Size(this.ChildSize, this.ChildSize); return new Size(width, height); } /// <summary> /// Position a child /// </summary> /// <param name="itemIndex">The data item index of the child</param> /// <param name="child">The element to position</param> /// <param name="finalSize">The size of the panel</param> private void ArrangeChild(int itemIndex, UIElement child, Size finalSize) { int childrenPerRow = CalculateChildrenPerRow(finalSize); int row = itemIndex / childrenPerRow; int column = itemIndex % childrenPerRow; child.Arrange(new Rect(column * width, row * height, width, height)); } /// <summary> /// Helper function for tiling layout /// </summary> /// <param name="availableSize">Size available</param> /// <returns></returns> private int CalculateChildrenPerRow(Size availableSize) { // Figure out how many children fit on each row int childrenPerRow; if (availableSize.Width == Double.PositiveInfinity) childrenPerRow = this.Children.Count; else childrenPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / width)); return childrenPerRow; } #endregion #region IScrollInfo Members private Size _extent = new Size(0, 0); private Size _viewport = new Size(0, 0); private Point _offset; private TranslateTransform _trans = new TranslateTransform(); private void UpdateScrollInfo(Size availableSize) { // See how many items there are ItemsControl itemsControl = ItemsControl.GetItemsOwner(this); int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0; Size extent = CalculateExtent(availableSize, itemCount); // Update extent if (extent != _extent) { _extent = extent; if (_owner != null) _owner.InvalidateScrollInfo(); } // Update viewport if (availableSize != _viewport) { _viewport = availableSize; if (_owner != null) _owner.InvalidateScrollInfo(); } } private bool _canHScroll = false; public bool CanHorizontallyScroll { get { return _canHScroll; } set { _canHScroll = value; } } private bool _canVScroll = false; public bool CanVerticallyScroll { get { return _canVScroll; } set { _canVScroll = value; } } public double ExtentHeight { get { return _extent.Height; } } public double ExtentWidth { get { return _extent.Width; } } public double HorizontalOffset { get { return _offset.X; } } public double VerticalOffset { get { return _offset.Y; } } public Rect MakeVisible(Visual visual, Rect rectangle) { return new Rect(); } public void MouseWheelDown() { PageDown(); } public void MouseWheelLeft() { throw new InvalidOperationException(); } public void MouseWheelRight() { throw new InvalidOperationException(); } public void MouseWheelUp() { PageUp(); } public void PageDown() { SetVerticalOffset(VerticalOffset + _viewport.Height * 0.1); } public void PageLeft() { SetHorizontalOffset(HorizontalOffset - _viewport.Width * 0.1); } public void PageRight() { SetHorizontalOffset(HorizontalOffset + _viewport.Width * 0.8); } public void PageUp() { SetVerticalOffset(VerticalOffset - _viewport.Height * 0.1); } private ScrollViewer _owner; public ScrollViewer ScrollOwner { get { return _owner; } set { _owner = value; } } public void SetHorizontalOffset(double offset) { if (offset < 0 || _viewport.Width >= _extent.Width) { offset = 0; } else { if (offset + _viewport.Width >= _extent.Width) { offset = _extent.Width - _viewport.Width; } } _offset.X = offset; if (_owner != null) _owner.InvalidateScrollInfo(); InvalidateMeasure(); } public void SetVerticalOffset(double offset) { if (offset < 0 || _viewport.Height >= _extent.Height) { offset = 0; } else { if (offset + _viewport.Height >= _extent.Height) { offset = _extent.Height - _viewport.Height; } } _offset.Y = offset; if (_owner != null) _owner.InvalidateScrollInfo(); _trans.Y = -offset; InvalidateMeasure(); } public double ViewportHeight { get { return _viewport.Height; } } public double ViewportWidth { get { return _viewport.Width; } } public void LineUp() { SetVerticalOffset(this.VerticalOffset - 10); } public void LineDown() { SetVerticalOffset(this.VerticalOffset + 10); } public void LineLeft() { throw new InvalidOperationException(); } public void LineRight() { throw new InvalidOperationException(); } #endregion #region helper data structures class ItemAbstraction { public ItemAbstraction(WrapPanelAbstraction panel, int index) { _panel = panel; _index = index; } WrapPanelAbstraction _panel; public readonly int _index; int _sectionIndex = -1; public int SectionIndex { get { if (_sectionIndex == -1) { return _index % _panel._averageItemsPerSection - 1; } return _sectionIndex; } set { if (_sectionIndex == -1) _sectionIndex = value; } } int _section = -1; public int Section { get { if (_section == -1) { return _index / _panel._averageItemsPerSection; } return _section; } set { if (_section == -1) _section = value; } } } class WrapPanelAbstraction : IEnumerable<ItemAbstraction> { public WrapPanelAbstraction(int itemCount) { List<ItemAbstraction> items = new List<ItemAbstraction>(itemCount); for (int i = 0; i < itemCount; i++) { ItemAbstraction item = new ItemAbstraction(this, i); items.Add(item); } Items = new ReadOnlyCollection<ItemAbstraction>(items); _averageItemsPerSection = itemCount; _itemCount = itemCount; } public readonly int _itemCount; public int _averageItemsPerSection; private int _currentSetSection = -1; private int _currentSetItemIndex = -1; private int _itemsInCurrentSecction = 0; private object _syncRoot = new object(); public int SectionCount { get { int ret = _currentSetSection + 1; if (_currentSetItemIndex + 1 < Items.Count) { int itemsLeft = Items.Count - _currentSetItemIndex; ret += itemsLeft / _averageItemsPerSection + 1; } return ret; } } private ReadOnlyCollection<ItemAbstraction> Items { get; set; } public void SetItemSection(int index, int section) { lock (_syncRoot) { if (section <= _currentSetSection + 1 && index == _currentSetItemIndex + 1) { _currentSetItemIndex++; Items[index].Section = section; if (section == _currentSetSection + 1) { _currentSetSection = section; if (section > 0) { _averageItemsPerSection = (index) / (section); } _itemsInCurrentSecction = 1; } else _itemsInCurrentSecction++; Items[index].SectionIndex = _itemsInCurrentSecction - 1; } } } public ItemAbstraction this[int index] { get { return Items[index]; } } #region IEnumerable<ItemAbstraction> Members public IEnumerator<ItemAbstraction> GetEnumerator() { return Items.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } #endregion } }
I then add this to my ListView like this:
<ListView.ItemsPanel><ItemsPanelTemplate><local:VirtualizingTilePanel><local:VirtualizingTilePanel.Background><ImageBrush ImageSource="..\Images\bookshelf.png" AlignmentX="Left" AlignmentY="Top" TileMode="Tile" Stretch="None" ViewportUnits="Absolute" Viewport="0,0,319,203" /></local:VirtualizingTilePanel.Background></local:VirtualizingTilePanel></ItemsPanelTemplate></ListView.ItemsPanel>
The problem I have is that the background only fills original visible area. Once I start scrolling down the background moves up (like I want it to) but the background that comes into view is just white.
How can I get the background fill the entire scrollable area?